diff --git a/.gitignore b/.gitignore index daf913b..a54c6a7 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,5 @@ _testmain.go *.exe *.test *.prof + +cf-statistics-plugin diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json new file mode 100644 index 0000000..f6ddfe2 --- /dev/null +++ b/Godeps/Godeps.json @@ -0,0 +1,41 @@ +{ + "ImportPath": "github.com/swisscom/cf-statistics-plugin", + "GoVersion": "go1.4.2", + "Packages": [ + "./..." + ], + "Deps": [ + { + "ImportPath": "github.com/cloudfoundry/cli/cf/configuration", + "Comment": "v6.11.2-34-g4cb5aa0", + "Rev": "4cb5aa08fb38826946dfdb5469868a21f6603db6" + }, + { + "ImportPath": "github.com/cloudfoundry/cli/cf/models", + "Comment": "v6.11.2-34-g4cb5aa0", + "Rev": "4cb5aa08fb38826946dfdb5469868a21f6603db6" + }, + { + "ImportPath": "github.com/cloudfoundry/cli/plugin", + "Comment": "v6.11.2-34-g4cb5aa0", + "Rev": "4cb5aa08fb38826946dfdb5469868a21f6603db6" + }, + { + "ImportPath": "github.com/gizak/termui", + "Rev": "9d0302382ddeed86cbedb11049dcab10f8131d15" + }, + { + "ImportPath": "github.com/mattn/go-runewidth", + "Comment": "travisish-31-g58a0da4", + "Rev": "58a0da4ed7b321c9b5dfeffb7e03ee188fae1c60" + }, + { + "ImportPath": "github.com/nsf/termbox-go", + "Rev": "bc836fdbcf75184c95c96500ea740e3dd727b5a8" + }, + { + "ImportPath": "github.com/pivotal-golang/bytefmt", + "Rev": "eb6a478daea5cd564c230719240761a1a2ccc713" + } + ] +} diff --git a/Godeps/Readme b/Godeps/Readme new file mode 100644 index 0000000..4cdaa53 --- /dev/null +++ b/Godeps/Readme @@ -0,0 +1,5 @@ +This directory tree is generated automatically by godep. + +Please do not edit. + +See https://github.com/tools/godep for more information. diff --git a/Godeps/_workspace/.gitignore b/Godeps/_workspace/.gitignore new file mode 100644 index 0000000..f037d68 --- /dev/null +++ b/Godeps/_workspace/.gitignore @@ -0,0 +1,2 @@ +/pkg +/bin diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/configuration/config_disk_persistor.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/configuration/config_disk_persistor.go new file mode 100644 index 0000000..7ab6755 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/configuration/config_disk_persistor.go @@ -0,0 +1,87 @@ +package configuration + +import ( + "io/ioutil" + "os" + "path/filepath" +) + +const ( + filePermissions = 0600 + dirPermissions = 0700 +) + +type Persistor interface { + Delete() + Exists() bool + Load(DataInterface) error + Save(DataInterface) error +} + +type DataInterface interface { + JsonMarshalV3() ([]byte, error) + JsonUnmarshalV3([]byte) error +} + +type DiskPersistor struct { + filePath string +} + +func NewDiskPersistor(path string) (dp DiskPersistor) { + return DiskPersistor{ + filePath: path, + } +} + +func (dp DiskPersistor) Exists() bool { + _, err := os.Stat(dp.filePath) + if err != nil && !os.IsExist(err) { + return false + } + return true +} + +func (dp DiskPersistor) Delete() { + os.Remove(dp.filePath) +} + +func (dp DiskPersistor) Load(data DataInterface) error { + err := dp.read(data) + if os.IsPermission(err) { + return err + } + + if err != nil { + err = dp.write(data) + } + return err +} + +func (dp DiskPersistor) Save(data DataInterface) (err error) { + return dp.write(data) +} + +func (dp DiskPersistor) read(data DataInterface) error { + err := os.MkdirAll(filepath.Dir(dp.filePath), dirPermissions) + if err != nil { + return err + } + + jsonBytes, err := ioutil.ReadFile(dp.filePath) + if err != nil { + return err + } + + err = data.JsonUnmarshalV3(jsonBytes) + return err +} + +func (dp DiskPersistor) write(data DataInterface) error { + bytes, err := data.JsonMarshalV3() + if err != nil { + return err + } + + err = ioutil.WriteFile(dp.filePath, bytes, filePermissions) + return err +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/configuration/config_disk_persistor_test.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/configuration/config_disk_persistor_test.go new file mode 100644 index 0000000..17d889d --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/configuration/config_disk_persistor_test.go @@ -0,0 +1,91 @@ +package configuration_test + +import ( + "encoding/json" + "io/ioutil" + "os" + + . "github.com/cloudfoundry/cli/cf/configuration" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("DiskPersistor", func() { + var ( + tmpDir string + tmpFile *os.File + diskPersistor DiskPersistor + ) + + BeforeEach(func() { + var err error + + tmpDir = os.TempDir() + + tmpFile, err = ioutil.TempFile(tmpDir, "tmp_file") + Expect(err).ToNot(HaveOccurred()) + + diskPersistor = NewDiskPersistor(tmpFile.Name()) + }) + + AfterEach(func() { + os.Remove(tmpFile.Name()) + }) + + Describe(".Delete", func() { + It("Deletes the correct file", func() { + tmpFile.Close() + diskPersistor.Delete() + + file, err := os.Stat(tmpFile.Name()) + Expect(file).To(BeNil()) + Expect(err).To(HaveOccurred()) + }) + }) + + Describe(".Save", func() { + It("Writes the json file to the correct filepath", func() { + d := &data{Info: "save test"} + + err := diskPersistor.Save(d) + Expect(err).ToNot(HaveOccurred()) + + dataBytes, err := ioutil.ReadFile(tmpFile.Name()) + Expect(err).ToNot(HaveOccurred()) + Expect(string(dataBytes)).To(ContainSubstring(d.Info)) + }) + }) + + Describe(".Load", func() { + It("Will load an empty json file", func() { + d := &data{} + + err := diskPersistor.Load(d) + Expect(err).ToNot(HaveOccurred()) + Expect(d.Info).To(Equal("")) + }) + + It("Will load a json file with specific keys", func() { + d := &data{} + + err := ioutil.WriteFile(tmpFile.Name(), []byte(`{"Info":"test string"}`), 0700) + Expect(err).ToNot(HaveOccurred()) + + err = diskPersistor.Load(d) + Expect(err).ToNot(HaveOccurred()) + Expect(d.Info).To(Equal("test string")) + }) + }) +}) + +type data struct { + Info string +} + +func (d *data) JsonMarshalV3() ([]byte, error) { + return json.MarshalIndent(d, "", " ") +} + +func (d *data) JsonUnmarshalV3(data []byte) error { + return json.Unmarshal(data, d) +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/configuration/config_helpers/config_helpers.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/configuration/config_helpers/config_helpers.go new file mode 100644 index 0000000..901c4a1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/configuration/config_helpers/config_helpers.go @@ -0,0 +1,43 @@ +package config_helpers + +import ( + "os" + "path/filepath" + "runtime" +) + +func DefaultFilePath() string { + var configDir string + + if os.Getenv("CF_HOME") != "" { + cfHome := os.Getenv("CF_HOME") + configDir = filepath.Join(cfHome, ".cf") + } else { + configDir = filepath.Join(userHomeDir(), ".cf") + } + + return filepath.Join(configDir, "config.json") +} + +// See: http://stackoverflow.com/questions/7922270/obtain-users-home-directory +// we can't cross compile using cgo and use user.Current() +var userHomeDir = func() string { + + if runtime.GOOS == "windows" { + home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") + if home == "" { + home = os.Getenv("USERPROFILE") + } + return home + } + + return os.Getenv("HOME") +} + +var PluginRepoDir = func() string { + if os.Getenv("CF_PLUGIN_HOME") != "" { + return os.Getenv("CF_PLUGIN_HOME") + } + + return userHomeDir() +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/configuration/configuration_suite_test.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/configuration/configuration_suite_test.go new file mode 100644 index 0000000..c983217 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/configuration/configuration_suite_test.go @@ -0,0 +1,13 @@ +package configuration_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestConfiguration(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Configuration Suite") +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/configuration/core_config/access_token.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/configuration/core_config/access_token.go new file mode 100644 index 0000000..336f6b2 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/configuration/core_config/access_token.go @@ -0,0 +1,56 @@ +package core_config + +import ( + "encoding/base64" + "encoding/json" + "strings" +) + +type TokenInfo struct { + Username string `json:"user_name"` + Email string `json:"email"` + UserGuid string `json:"user_id"` +} + +func NewTokenInfo(accessToken string) (info TokenInfo) { + tokenJson, err := DecodeAccessToken(accessToken) + + if err != nil { + return + } + info = TokenInfo{} + err = json.Unmarshal(tokenJson, &info) + return +} + +func DecodeAccessToken(accessToken string) (tokenJson []byte, err error) { + tokenParts := strings.Split(accessToken, " ") + + if len(tokenParts) < 2 { + return + } + + token := tokenParts[1] + encodedParts := strings.Split(token, ".") + + if len(encodedParts) < 3 { + return + } + + encodedTokenJson := encodedParts[1] + return base64Decode(encodedTokenJson) +} + +func base64Decode(encodedData string) ([]byte, error) { + return base64.StdEncoding.DecodeString(restorePadding(encodedData)) +} + +func restorePadding(seg string) string { + switch len(seg) % 4 { + case 2: + seg = seg + "==" + case 3: + seg = seg + "=" + } + return seg +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/configuration/core_config/access_token_test.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/configuration/core_config/access_token_test.go new file mode 100644 index 0000000..f83c2d1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/configuration/core_config/access_token_test.go @@ -0,0 +1,33 @@ +package core_config_test + +import ( + . "github.com/cloudfoundry/cli/cf/configuration/core_config" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("NewTokenInfo", func() { + It("decodes a string into TokenInfo when there is no padding present", func() { + accessToken := "bearer eyJhbGciOiJSUzI1NiJ9.eyJqdGkiOiJjNDE4OTllNS1kZTE1LTQ5NGQtYWFiNC04ZmNlYzUxN2UwMDUiLCJzdWIiOiI3NzJkZGEzZi02NjlmLTQyNzYtYjJiZC05MDQ4NmFiZTFmNmYiLCJzY29wZSI6WyJjbG91ZF9jb250cm9sbGVyLnJlYWQiLCJjbG91ZF9jb250cm9sbGVyLndyaXRlIiwib3BlbmlkIiwicGFzc3dvcmQud3JpdGUiXSwiY2xpZW50X2lkIjoiY2YiLCJjaWQiOiJjZiIsImdyYW50X3R5cGUiOiJwYXNzd29yZCIsInVzZXJfaWQiOiI3NzJkZGEzZi02NjlmLTQyNzYtYjJiZC05MDQ4NmFiZTFmNmYiLCJ1c2VyX25hbWUiOiJ1c2VyMUBleGFtcGxlLmNvbSIsImVtYWlsIjoidXNlcjFAZXhhbXBsZS5jb20iLCJpYXQiOjEzNzcwMjgzNTYsImV4cCI6MTM3NzAzNTU1NiwiaXNzIjoiaHR0cHM6Ly91YWEuYXJib3JnbGVuLmNmLWFwcC5jb20vb2F1dGgvdG9rZW4iLCJhdWQiOlsib3BlbmlkIiwiY2xvdWRfY29udHJvbGxlciIsInBhc3N3b3JkIl19.kjFJHi0Qir9kfqi2eyhHy6kdewhicAFu8hrPR1a5AxFvxGB45slKEjuP0_72cM_vEYICgZn3PcUUkHU9wghJO9wjZ6kiIKK1h5f2K9g-Iprv9BbTOWUODu1HoLIvg2TtGsINxcRYy_8LW1RtvQc1b4dBPoopaEH4no-BIzp0E5E" + decodedInfo, err := DecodeAccessToken(accessToken) + + Expect(err).NotTo(HaveOccurred()) + Expect(string(decodedInfo)).To(ContainSubstring("user1@example.com")) + }) + + It("decodes a string into TokenInfo when there is doubling padding present", func() { + accessToken := "bearer eyJhbGciOiJSUzI1NiJ9.eyJqdGkiOiIwNTg2MjlkNC04NjEwLTQ3NTEtOTg3Ny0yOGMwNzE3YTE5ZTciLCJzdWIiOiIzNGFiMDhkOC04YmVmLTQ1MzQtOGYyOC0zODhhYWI1MjAwMmEiLCJzY29wZSI6WyJjbG91ZF9jb250cm9sbGVyLnJlYWQiLCJjbG91ZF9jb250cm9sbGVyLndyaXRlIiwib3BlbmlkIiwicGFzc3dvcmQud3JpdGUiXSwiY2xpZW50X2lkIjoiY2YiLCJjaWQiOiJjZiIsImdyYW50X3R5cGUiOiJwYXNzd29yZCIsInVzZXJfaWQiOiIzNGFiMDhkOC04YmVmLTQ1MzQtOGYyOC0zODhhYWI1MjAwMmEiLCJ1c2VyX25hbWUiOiJ0bGFuZ0Bnb3Bpdm90YWwuY29tIiwiZW1haWwiOiJ0bGFuZ0Bnb3Bpdm90YWwuY29tIiwiaWF0IjoxMzc3MDk1ODM5LCJleHAiOjEzNzcxMzkwMzksImlzcyI6Imh0dHBzOi8vdWFhLnJ1bi5waXZvdGFsLmlvL29hdXRoL3Rva2VuIiwiYXVkIjpbIm9wZW5pZCIsImNsb3VkX2NvbnRyb2xsZXIiLCJwYXNzd29yZCJdfQ.dcgrGjPvTjYvg8dTSZY5ecZZTNt59IYd442VaEXXvLNB_WQCAdbVOxiJ14ogzQkkzDDw60Q2lbw4z6HrqM1a-BNpYfRmvaIP_79GpIZC6OzQy_PgA1whL27pO7_ABkSJT1CEgJQJMTQlYOiZNHvFTWen3G4O6ey680cxIN5VvbFjmmQHCuwANE9_GqnYYvoI9tS1nERku8DX2H9KH5NAgDa52-p0NhLnZRqYjGss6EyPYkwYN5w2OizfYUmEYVWo8K1Q45_TGMoE-LgZe2mGWwv0euLYBoFTkYhtBMj91dQagLrL1aGcmDKPc6ivkXtfpN4Zv7FJ9OXJ2DPQyHKRpw" + decodedInfo, err := DecodeAccessToken(accessToken) + + Expect(err).NotTo(HaveOccurred()) + Expect(string(decodedInfo)).To(ContainSubstring("tlang@gopivotal.com")) + }) + + It("decodes a string into TokenInfo when there is single padding present", func() { + accessToken := "bearer eyJhbGciOiJSUzI1NiJ9.eyJqdGkiOiIwNTg2MjlkNC04NjEwLTQ3NTEtOTg3Ny0yOGMwNzE3YTE5ZTciLCJzdWIiOiIzNGFiMDhkOC04YmVmLTQ1MzQtOGYyOC0zODhhYWI1MjAwMmEiLCJzY29wZSI6WyJjbG91ZF9jb250cm9sbGVyLnJlYWQiLCJjbG91ZF9jb250cm9sbGVyLndyaXRlIiwib3BlbmlkIiwicGFzc3dvcmQud3JpdGUiXSwiY2xpZW50X2lkIjoiY2YiLCJjaWQiOiJjZiIsImdyYW50X3R5cGUiOiJwYXNzd29yZCIsInVzZXJfaWQiOiIzNGFiMDhkOC04YmVmLTQ1MzQtOGYyOC0zODhhYWI1MjAwMmEiLCJ1c2VyX25hbWUiOiJ0bGFuZzFAZ29waXZvdGFsLmNvbSIsImVtYWlsIjoidGxhbmdAZ29waXZvdGFsLmNvbSIsImlhdCI6MTM3NzA5NTgzOSwiZXhwIjoxMzc3MTM5MDM5LCJpc3MiOiJodHRwczovL3VhYS5ydW4ucGl2b3RhbC5pby9vYXV0aC90b2tlbiIsImF1ZCI6WyJvcGVuaWQiLCJjbG91ZF9jb250cm9sbGVyIiwicGFzc3dvcmQiXX0.dcgrGjPvTjYvg8dTSZY5ecZZTNt59IYd442VaEXXvLNB_WQCAdbVOxiJ14ogzQkkzDDw60Q2lbw4z6HrqM1a-BNpYfRmvaIP_79GpIZC6OzQy_PgA1whL27pO7_ABkSJT1CEgJQJMTQlYOiZNHvFTWen3G4O6ey680cxIN5VvbFjmmQHCuwANE9_GqnYYvoI9tS1nERku8DX2H9KH5NAgDa52-p0NhLnZRqYjGss6EyPYkwYN5w2OizfYUmEYVWo8K1Q45_TGMoE-LgZe2mGWwv0euLYBoFTkYhtBMj91dQagLrL1aGcmDKPc6ivkXtfpN4Zv7FJ9OXJ2DPQyHKRpw" + decodedInfo, err := DecodeAccessToken(accessToken) + + Expect(err).NotTo(HaveOccurred()) + Expect(string(decodedInfo)).To(ContainSubstring("tlang1@gopivotal.com")) + }) +}) diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/configuration/core_config/config_data.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/configuration/core_config/config_data.go new file mode 100644 index 0000000..0ae3b18 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/configuration/core_config/config_data.go @@ -0,0 +1,65 @@ +package core_config + +import ( + "encoding/json" + + "github.com/cloudfoundry/cli/cf/models" +) + +type AuthPromptType string + +const ( + AuthPromptTypeText AuthPromptType = "TEXT" + AuthPromptTypePassword AuthPromptType = "PASSWORD" +) + +type AuthPrompt struct { + Type AuthPromptType + DisplayName string +} + +type Data struct { + ConfigVersion int + Target string + ApiVersion string + AuthorizationEndpoint string + LoggregatorEndPoint string + DopplerEndPoint string + UaaEndpoint string + AccessToken string + RefreshToken string + OrganizationFields models.OrganizationFields + SpaceFields models.SpaceFields + SSLDisabled bool + AsyncTimeout uint + Trace string + ColorEnabled string + Locale string + PluginRepos []models.PluginRepo + MinCliVersion string + MinRecommendedCliVersion string +} + +func NewData() (data *Data) { + data = new(Data) + return +} + +func (d *Data) JsonMarshalV3() (output []byte, err error) { + d.ConfigVersion = 3 + return json.MarshalIndent(d, "", " ") +} + +func (d *Data) JsonUnmarshalV3(input []byte) (err error) { + err = json.Unmarshal(input, d) + if err != nil { + return + } + + if d.ConfigVersion != 3 { + *d = Data{} + return + } + + return +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/configuration/core_config/config_data_test.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/configuration/core_config/config_data_test.go new file mode 100644 index 0000000..457b7ce --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/configuration/core_config/config_data_test.go @@ -0,0 +1,118 @@ +package core_config_test + +import ( + "regexp" + + . "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var exampleJSON = ` +{ + "ConfigVersion": 3, + "Target": "api.example.com", + "ApiVersion": "3", + "AuthorizationEndpoint": "auth.example.com", + "LoggregatorEndPoint": "loggregator.example.com", + "DopplerEndPoint": "doppler.example.com", + "UaaEndpoint": "uaa.example.com", + "AccessToken": "the-access-token", + "RefreshToken": "the-refresh-token", + "OrganizationFields": { + "Guid": "the-org-guid", + "Name": "the-org", + "QuotaDefinition": { + "name":"", + "memory_limit":0, + "instance_memory_limit":0, + "total_routes":0, + "total_services":0, + "non_basic_services_allowed": false + } + }, + "SpaceFields": { + "Guid": "the-space-guid", + "Name": "the-space" + }, + "SSLDisabled": true, + "AsyncTimeout": 1000, + "Trace": "path/to/some/file", + "ColorEnabled": "true", + "Locale": "fr_FR", + "PluginRepos": [ + { + "Name": "repo1", + "Url": "http://repo.com" + } + ], + "MinCliVersion": "6.0.0", + "MinRecommendedCliVersion": "6.9.0" +}` + +var exampleData = &Data{ + Target: "api.example.com", + ApiVersion: "3", + AuthorizationEndpoint: "auth.example.com", + LoggregatorEndPoint: "loggregator.example.com", + DopplerEndPoint: "doppler.example.com", + UaaEndpoint: "uaa.example.com", + AccessToken: "the-access-token", + RefreshToken: "the-refresh-token", + MinCliVersion: "6.0.0", + MinRecommendedCliVersion: "6.9.0", + OrganizationFields: models.OrganizationFields{ + Guid: "the-org-guid", + Name: "the-org", + }, + SpaceFields: models.SpaceFields{ + Guid: "the-space-guid", + Name: "the-space", + }, + SSLDisabled: true, + Trace: "path/to/some/file", + AsyncTimeout: 1000, + ColorEnabled: "true", + Locale: "fr_FR", + PluginRepos: []models.PluginRepo{ + models.PluginRepo{ + Name: "repo1", + Url: "http://repo.com", + }, + }, +} + +var _ = Describe("V3 Config files", func() { + Describe("serialization", func() { + It("creates a JSON string from the config object", func() { + jsonData, err := exampleData.JsonMarshalV3() + + Expect(err).NotTo(HaveOccurred()) + Expect(stripWhitespace(string(jsonData))).To(ContainSubstring(stripWhitespace(exampleJSON))) + }) + }) + + Describe("parsing", func() { + It("returns an error when the JSON is invalid", func() { + configData := NewData() + err := configData.JsonUnmarshalV3([]byte(`{ "not_valid": ### }`)) + + Expect(err).To(HaveOccurred()) + }) + + It("creates a config object from valid JSON", func() { + configData := NewData() + err := configData.JsonUnmarshalV3([]byte(exampleJSON)) + + Expect(err).NotTo(HaveOccurred()) + Expect(configData).To(Equal(exampleData)) + }) + }) +}) + +var whiteSpaceRegex = regexp.MustCompile(`\s+`) + +func stripWhitespace(input string) string { + return whiteSpaceRegex.ReplaceAllString(input, "") +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/configuration/core_config/config_repository.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/configuration/core_config/config_repository.go new file mode 100644 index 0000000..80af6ba --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/configuration/core_config/config_repository.go @@ -0,0 +1,475 @@ +package core_config + +import ( + "strings" + "sync" + + "github.com/cloudfoundry/cli/cf/configuration" + "github.com/cloudfoundry/cli/cf/models" +) + +type ConfigRepository struct { + data *Data + mutex *sync.RWMutex + initOnce *sync.Once + persistor configuration.Persistor + onError func(error) +} + +func NewRepositoryFromFilepath(filepath string, errorHandler func(error)) Repository { + return NewRepositoryFromPersistor(configuration.NewDiskPersistor(filepath), errorHandler) +} + +func NewRepositoryFromPersistor(persistor configuration.Persistor, errorHandler func(error)) Repository { + data := NewData() + if !persistor.Exists() { + //set default plugin repo + data.PluginRepos = append(data.PluginRepos, models.PluginRepo{ + Name: "CF-Community", + Url: "http://plugins.cloudfoundry.org", + }) + } + + return &ConfigRepository{ + data: data, + mutex: new(sync.RWMutex), + initOnce: new(sync.Once), + persistor: persistor, + onError: errorHandler, + } +} + +type Reader interface { + ApiEndpoint() string + ApiVersion() string + HasAPIEndpoint() bool + + AuthenticationEndpoint() string + LoggregatorEndpoint() string + DopplerEndpoint() string + UaaEndpoint() string + AccessToken() string + RefreshToken() string + + OrganizationFields() models.OrganizationFields + HasOrganization() bool + + SpaceFields() models.SpaceFields + HasSpace() bool + + Username() string + UserGuid() string + UserEmail() string + IsLoggedIn() bool + IsSSLDisabled() bool + IsMinApiVersion(string) bool + IsMinCliVersion(string) bool + MinCliVersion() string + MinRecommendedCliVersion() string + + AsyncTimeout() uint + Trace() string + + ColorEnabled() string + + Locale() string + + PluginRepos() []models.PluginRepo +} + +type ReadWriter interface { + Reader + ClearSession() + SetApiEndpoint(string) + SetApiVersion(string) + SetMinCliVersion(string) + SetMinRecommendedCliVersion(string) + SetAuthenticationEndpoint(string) + SetLoggregatorEndpoint(string) + SetDopplerEndpoint(string) + SetUaaEndpoint(string) + SetAccessToken(string) + SetRefreshToken(string) + SetOrganizationFields(models.OrganizationFields) + SetSpaceFields(models.SpaceFields) + SetSSLDisabled(bool) + SetAsyncTimeout(uint) + SetTrace(string) + SetColorEnabled(string) + SetLocale(string) + SetPluginRepo(models.PluginRepo) + UnSetPluginRepo(int) +} + +type Repository interface { + ReadWriter + Close() +} + +// ACCESS CONTROL + +func (c *ConfigRepository) init() { + c.initOnce.Do(func() { + err := c.persistor.Load(c.data) + if err != nil { + c.onError(err) + } + }) +} + +func (c *ConfigRepository) read(cb func()) { + c.mutex.RLock() + defer c.mutex.RUnlock() + c.init() + + cb() +} + +func (c *ConfigRepository) write(cb func()) { + c.mutex.Lock() + defer c.mutex.Unlock() + c.init() + + cb() + + err := c.persistor.Save(c.data) + if err != nil { + c.onError(err) + } +} + +// CLOSERS + +func (c *ConfigRepository) Close() { + c.read(func() { + // perform a read to ensure write lock has been cleared + }) +} + +// GETTERS + +func (c *ConfigRepository) ApiVersion() (apiVersion string) { + c.read(func() { + apiVersion = c.data.ApiVersion + }) + return +} + +func (c *ConfigRepository) AuthenticationEndpoint() (authEndpoint string) { + c.read(func() { + authEndpoint = c.data.AuthorizationEndpoint + }) + return +} + +func (c *ConfigRepository) LoggregatorEndpoint() (logEndpoint string) { + c.read(func() { + logEndpoint = c.data.LoggregatorEndPoint + }) + return +} + +func (c *ConfigRepository) DopplerEndpoint() (logEndpoint string) { + //revert this in v7.0, once CC advertise doppler endpoint, and + //everyone has migrated from loggregator to doppler + + // c.read(func() { + // logEndpoint = c.data.DopplerEndPoint + // }) + c.read(func() { + logEndpoint = c.data.LoggregatorEndPoint + }) + + return strings.Replace(logEndpoint, "loggregator", "doppler", 1) +} + +func (c *ConfigRepository) UaaEndpoint() (uaaEndpoint string) { + c.read(func() { + uaaEndpoint = c.data.UaaEndpoint + }) + return +} + +func (c *ConfigRepository) ApiEndpoint() (apiEndpoint string) { + c.read(func() { + apiEndpoint = c.data.Target + }) + return +} + +func (c *ConfigRepository) HasAPIEndpoint() (hasEndpoint bool) { + c.read(func() { + hasEndpoint = c.data.ApiVersion != "" && c.data.Target != "" + }) + return +} + +func (c *ConfigRepository) AccessToken() (accessToken string) { + c.read(func() { + accessToken = c.data.AccessToken + }) + return +} + +func (c *ConfigRepository) RefreshToken() (refreshToken string) { + c.read(func() { + refreshToken = c.data.RefreshToken + }) + return +} + +func (c *ConfigRepository) OrganizationFields() (org models.OrganizationFields) { + c.read(func() { + org = c.data.OrganizationFields + }) + return +} + +func (c *ConfigRepository) SpaceFields() (space models.SpaceFields) { + c.read(func() { + space = c.data.SpaceFields + }) + return +} + +func (c *ConfigRepository) UserEmail() (email string) { + c.read(func() { + email = NewTokenInfo(c.data.AccessToken).Email + }) + return +} + +func (c *ConfigRepository) UserGuid() (guid string) { + c.read(func() { + guid = NewTokenInfo(c.data.AccessToken).UserGuid + }) + return +} + +func (c *ConfigRepository) Username() (name string) { + c.read(func() { + name = NewTokenInfo(c.data.AccessToken).Username + }) + return +} + +func (c *ConfigRepository) IsLoggedIn() (loggedIn bool) { + c.read(func() { + loggedIn = c.data.AccessToken != "" + }) + return +} + +func (c *ConfigRepository) HasOrganization() (hasOrg bool) { + c.read(func() { + hasOrg = c.data.OrganizationFields.Guid != "" && c.data.OrganizationFields.Name != "" + }) + return +} + +func (c *ConfigRepository) HasSpace() (hasSpace bool) { + c.read(func() { + hasSpace = c.data.SpaceFields.Guid != "" && c.data.SpaceFields.Name != "" + }) + return +} + +func (c *ConfigRepository) IsSSLDisabled() (isSSLDisabled bool) { + c.read(func() { + isSSLDisabled = c.data.SSLDisabled + }) + return +} + +func (c *ConfigRepository) IsMinApiVersion(v string) bool { + var apiVersion string + c.read(func() { + apiVersion = c.data.ApiVersion + }) + return apiVersion >= v +} + +func (c *ConfigRepository) IsMinCliVersion(version string) bool { + if version == "BUILT_FROM_SOURCE" { + return true + } + var minCliVersion string + c.read(func() { + minCliVersion = c.data.MinCliVersion + }) + return version >= minCliVersion +} + +func (c *ConfigRepository) MinCliVersion() (minCliVersion string) { + c.read(func() { + minCliVersion = c.data.MinCliVersion + }) + return +} + +func (c *ConfigRepository) MinRecommendedCliVersion() (minRecommendedCliVersion string) { + c.read(func() { + minRecommendedCliVersion = c.data.MinRecommendedCliVersion + }) + return +} + +func (c *ConfigRepository) AsyncTimeout() (timeout uint) { + c.read(func() { + timeout = c.data.AsyncTimeout + }) + return +} + +func (c *ConfigRepository) Trace() (trace string) { + c.read(func() { + trace = c.data.Trace + }) + return +} + +func (c *ConfigRepository) ColorEnabled() (enabled string) { + c.read(func() { + enabled = c.data.ColorEnabled + }) + return +} + +func (c *ConfigRepository) Locale() (locale string) { + c.read(func() { + locale = c.data.Locale + }) + return +} + +func (c *ConfigRepository) PluginRepos() (repos []models.PluginRepo) { + c.read(func() { + repos = c.data.PluginRepos + }) + return +} + +// SETTERS + +func (c *ConfigRepository) ClearSession() { + c.write(func() { + c.data.AccessToken = "" + c.data.RefreshToken = "" + c.data.OrganizationFields = models.OrganizationFields{} + c.data.SpaceFields = models.SpaceFields{} + }) +} + +func (c *ConfigRepository) SetApiEndpoint(endpoint string) { + c.write(func() { + c.data.Target = endpoint + }) +} + +func (c *ConfigRepository) SetApiVersion(version string) { + c.write(func() { + c.data.ApiVersion = version + }) +} + +func (c *ConfigRepository) SetMinCliVersion(version string) { + c.write(func() { + c.data.MinCliVersion = version + }) +} + +func (c *ConfigRepository) SetMinRecommendedCliVersion(version string) { + c.write(func() { + c.data.MinRecommendedCliVersion = version + }) +} + +func (c *ConfigRepository) SetAuthenticationEndpoint(endpoint string) { + c.write(func() { + c.data.AuthorizationEndpoint = endpoint + }) +} + +func (c *ConfigRepository) SetLoggregatorEndpoint(endpoint string) { + c.write(func() { + c.data.LoggregatorEndPoint = endpoint + }) +} + +func (c *ConfigRepository) SetDopplerEndpoint(endpoint string) { + c.write(func() { + c.data.DopplerEndPoint = endpoint + }) +} + +func (c *ConfigRepository) SetUaaEndpoint(uaaEndpoint string) { + c.write(func() { + c.data.UaaEndpoint = uaaEndpoint + }) +} + +func (c *ConfigRepository) SetAccessToken(token string) { + c.write(func() { + c.data.AccessToken = token + }) +} + +func (c *ConfigRepository) SetRefreshToken(token string) { + c.write(func() { + c.data.RefreshToken = token + }) +} + +func (c *ConfigRepository) SetOrganizationFields(org models.OrganizationFields) { + c.write(func() { + c.data.OrganizationFields = org + }) +} + +func (c *ConfigRepository) SetSpaceFields(space models.SpaceFields) { + c.write(func() { + c.data.SpaceFields = space + }) +} + +func (c *ConfigRepository) SetSSLDisabled(disabled bool) { + c.write(func() { + c.data.SSLDisabled = disabled + }) +} + +func (c *ConfigRepository) SetAsyncTimeout(timeout uint) { + c.write(func() { + c.data.AsyncTimeout = timeout + }) +} + +func (c *ConfigRepository) SetTrace(value string) { + c.write(func() { + c.data.Trace = value + }) +} + +func (c *ConfigRepository) SetColorEnabled(enabled string) { + c.write(func() { + c.data.ColorEnabled = enabled + }) +} + +func (c *ConfigRepository) SetLocale(locale string) { + c.write(func() { + c.data.Locale = locale + }) +} + +func (c *ConfigRepository) SetPluginRepo(repo models.PluginRepo) { + c.write(func() { + c.data.PluginRepos = append(c.data.PluginRepos, repo) + }) +} + +func (c *ConfigRepository) UnSetPluginRepo(index int) { + c.write(func() { + c.data.PluginRepos = append(c.data.PluginRepos[:index], c.data.PluginRepos[index+1:]...) + }) +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/configuration/core_config/config_repository_test.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/configuration/core_config/config_repository_test.go new file mode 100644 index 0000000..35c556d --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/configuration/core_config/config_repository_test.go @@ -0,0 +1,193 @@ +package core_config_test + +import ( + "fmt" + "os" + "path/filepath" + "time" + + . "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/fileutils" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + "github.com/cloudfoundry/cli/testhelpers/maker" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Configuration Repository", func() { + var config Repository + var repo *testconfig.FakePersistor + + BeforeEach(func() { + repo = testconfig.NewFakePersistor() + repo.LoadReturns.Data = NewData() + config = testconfig.NewRepository() + }) + + It("is safe for concurrent reading and writing", func() { + swapValLoop := func(config Repository) { + for { + val := config.ApiEndpoint() + + switch val { + case "foo": + config.SetApiEndpoint("bar") + case "bar": + config.SetApiEndpoint("foo") + default: + panic(fmt.Sprintf("WAT: %s", val)) + } + } + } + + config.SetApiEndpoint("foo") + + go swapValLoop(config) + go swapValLoop(config) + go swapValLoop(config) + go swapValLoop(config) + + time.Sleep(10 * time.Millisecond) + }) + + // TODO - test ClearTokens et al + It("has acccessor methods for all config fields", func() { + config.SetApiEndpoint("http://api.the-endpoint") + Expect(config.ApiEndpoint()).To(Equal("http://api.the-endpoint")) + + config.SetApiVersion("3") + Expect(config.ApiVersion()).To(Equal("3")) + + config.SetAuthenticationEndpoint("http://auth.the-endpoint") + Expect(config.AuthenticationEndpoint()).To(Equal("http://auth.the-endpoint")) + + config.SetLoggregatorEndpoint("http://loggregator.the-endpoint") + Expect(config.LoggregatorEndpoint()).To(Equal("http://loggregator.the-endpoint")) + + config.SetDopplerEndpoint("http://doppler.the-endpoint") + Expect(config.DopplerEndpoint()).To(Equal("http://doppler.the-endpoint")) + + config.SetUaaEndpoint("http://uaa.the-endpoint") + Expect(config.UaaEndpoint()).To(Equal("http://uaa.the-endpoint")) + + config.SetAccessToken("the-token") + Expect(config.AccessToken()).To(Equal("the-token")) + + config.SetRefreshToken("the-token") + Expect(config.RefreshToken()).To(Equal("the-token")) + + organization := maker.NewOrgFields(maker.Overrides{"name": "the-org"}) + config.SetOrganizationFields(organization) + Expect(config.OrganizationFields()).To(Equal(organization)) + + space := maker.NewSpaceFields(maker.Overrides{"name": "the-space"}) + config.SetSpaceFields(space) + Expect(config.SpaceFields()).To(Equal(space)) + + config.SetSSLDisabled(false) + Expect(config.IsSSLDisabled()).To(BeFalse()) + + config.SetLocale("en_US") + Expect(config.Locale()).To(Equal("en_US")) + + config.SetPluginRepo(models.PluginRepo{Name: "repo", Url: "nowhere.com"}) + Expect(config.PluginRepos()[0].Name).To(Equal("repo")) + Expect(config.PluginRepos()[0].Url).To(Equal("nowhere.com")) + + Expect(config.IsMinApiVersion("3.1")).To(Equal(false)) + + config.SetMinCliVersion("6.0.0") + Expect(config.IsMinCliVersion("5.0.0")).To(Equal(false)) + Expect(config.MinCliVersion()).To(Equal("6.0.0")) + + config.SetMinRecommendedCliVersion("6.9.0") + Expect(config.MinRecommendedCliVersion()).To(Equal("6.9.0")) + }) + + Describe("HasAPIEndpoint", func() { + Context("when both endpoint and version are set", func() { + BeforeEach(func() { + config.SetApiEndpoint("http://example.org") + config.SetApiVersion("42.1.2.3") + }) + It("returns true", func() { + Expect(config.HasAPIEndpoint()).To(BeTrue()) + }) + }) + + Context("when endpoint is not set", func() { + BeforeEach(func() { + config.SetApiVersion("42.1.2.3") + }) + It("returns false", func() { + Expect(config.HasAPIEndpoint()).To(BeFalse()) + }) + }) + + Context("when version is not set", func() { + BeforeEach(func() { + config.SetApiEndpoint("http://example.org") + }) + It("returns false", func() { + Expect(config.HasAPIEndpoint()).To(BeFalse()) + }) + }) + }) + + It("User has a valid Access Token", func() { + config.SetAccessToken("bearer eyJhbGciOiJSUzI1NiJ9.eyJqdGkiOiJjNDE4OTllNS1kZTE1LTQ5NGQtYWFiNC04ZmNlYzUxN2UwMDUiLCJzdWIiOiI3NzJkZGEzZi02NjlmLTQyNzYtYjJiZC05MDQ4NmFiZTFmNmYiLCJzY29wZSI6WyJjbG91ZF9jb250cm9sbGVyLnJlYWQiLCJjbG91ZF9jb250cm9sbGVyLndyaXRlIiwib3BlbmlkIiwicGFzc3dvcmQud3JpdGUiXSwiY2xpZW50X2lkIjoiY2YiLCJjaWQiOiJjZiIsImdyYW50X3R5cGUiOiJwYXNzd29yZCIsInVzZXJfaWQiOiI3NzJkZGEzZi02NjlmLTQyNzYtYjJiZC05MDQ4NmFiZTFmNmYiLCJ1c2VyX25hbWUiOiJ1c2VyMUBleGFtcGxlLmNvbSIsImVtYWlsIjoidXNlcjFAZXhhbXBsZS5jb20iLCJpYXQiOjEzNzcwMjgzNTYsImV4cCI6MTM3NzAzNTU1NiwiaXNzIjoiaHR0cHM6Ly91YWEuYXJib3JnbGVuLmNmLWFwcC5jb20vb2F1dGgvdG9rZW4iLCJhdWQiOlsib3BlbmlkIiwiY2xvdWRfY29udHJvbGxlciIsInBhc3N3b3JkIl19.kjFJHi0Qir9kfqi2eyhHy6kdewhicAFu8hrPR1a5AxFvxGB45slKEjuP0_72cM_vEYICgZn3PcUUkHU9wghJO9wjZ6kiIKK1h5f2K9g-Iprv9BbTOWUODu1HoLIvg2TtGsINxcRYy_8LW1RtvQc1b4dBPoopaEH4no-BIzp0E5E") + Expect(config.UserGuid()).To(Equal("772dda3f-669f-4276-b2bd-90486abe1f6f")) + Expect(config.UserEmail()).To(Equal("user1@example.com")) + }) + + It("User has an invalid Access Token", func() { + config.SetAccessToken("bearer") + Expect(config.UserGuid()).To(BeEmpty()) + Expect(config.UserEmail()).To(BeEmpty()) + + config.SetAccessToken("bearer eyJhbGciOiJSUzI1NiJ9") + Expect(config.UserGuid()).To(BeEmpty()) + Expect(config.UserEmail()).To(BeEmpty()) + }) + + It("has sane defaults when there is no config to read", func() { + withFakeHome(func(configPath string) { + config = NewRepositoryFromFilepath(configPath, func(err error) { + panic(err) + }) + + Expect(config.ApiEndpoint()).To(Equal("")) + Expect(config.ApiVersion()).To(Equal("")) + Expect(config.AuthenticationEndpoint()).To(Equal("")) + Expect(config.AccessToken()).To(Equal("")) + }) + }) + + Context("when the configuration version is older than the current version", func() { + It("returns a new empty config", func() { + withConfigFixture("outdated-config", func(configPath string) { + config = NewRepositoryFromFilepath(configPath, func(err error) { + panic(err) + }) + + Expect(config.ApiEndpoint()).To(Equal("")) + }) + }) + }) +}) + +func withFakeHome(callback func(dirPath string)) { + fileutils.TempDir("test-config", func(dir string, err error) { + if err != nil { + Fail("Couldn't create tmp file") + } + callback(filepath.Join(dir, ".cf", "config.json")) + }) +} + +func withConfigFixture(name string, callback func(dirPath string)) { + cwd, err := os.Getwd() + Expect(err).NotTo(HaveOccurred()) + callback(filepath.Join(cwd, "..", "..", "..", "fixtures", "config", name, ".cf", "config.json")) +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/configuration/core_config/core_config_suite_test.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/configuration/core_config/core_config_suite_test.go new file mode 100644 index 0000000..1a13f05 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/configuration/core_config/core_config_suite_test.go @@ -0,0 +1,13 @@ +package core_config_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestCoreConfig(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "CoreConfig Suite") +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/configuration/fakes/fake_repository.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/configuration/fakes/fake_repository.go new file mode 100644 index 0000000..dc0f766 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/configuration/fakes/fake_repository.go @@ -0,0 +1,1139 @@ +// This file was generated by counterfeiter +package fakes + +import ( + . "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + + "sync" +) + +type FakeRepository struct { + ApiEndpointStub func() string + apiEndpointMutex sync.RWMutex + apiEndpointArgsForCall []struct{} + apiEndpointReturns struct { + result1 string + } + ApiVersionStub func() string + apiVersionMutex sync.RWMutex + apiVersionArgsForCall []struct{} + apiVersionReturns struct { + result1 string + } + HasAPIEndpointStub func() bool + hasAPIEndpointMutex sync.RWMutex + hasAPIEndpointArgsForCall []struct{} + hasAPIEndpointReturns struct { + result1 bool + } + AuthenticationEndpointStub func() string + authenticationEndpointMutex sync.RWMutex + authenticationEndpointArgsForCall []struct{} + authenticationEndpointReturns struct { + result1 string + } + LoggregatorEndpointStub func() string + loggregatorEndpointMutex sync.RWMutex + loggregatorEndpointArgsForCall []struct{} + loggregatorEndpointReturns struct { + result1 string + } + UaaEndpointStub func() string + uaaEndpointMutex sync.RWMutex + uaaEndpointArgsForCall []struct{} + uaaEndpointReturns struct { + result1 string + } + AccessTokenStub func() string + accessTokenMutex sync.RWMutex + accessTokenArgsForCall []struct{} + accessTokenReturns struct { + result1 string + } + RefreshTokenStub func() string + refreshTokenMutex sync.RWMutex + refreshTokenArgsForCall []struct{} + refreshTokenReturns struct { + result1 string + } + OrganizationFieldsStub func() models.OrganizationFields + organizationFieldsMutex sync.RWMutex + organizationFieldsArgsForCall []struct{} + organizationFieldsReturns struct { + result1 models.OrganizationFields + } + HasOrganizationStub func() bool + hasOrganizationMutex sync.RWMutex + hasOrganizationArgsForCall []struct{} + hasOrganizationReturns struct { + result1 bool + } + SpaceFieldsStub func() models.SpaceFields + spaceFieldsMutex sync.RWMutex + spaceFieldsArgsForCall []struct{} + spaceFieldsReturns struct { + result1 models.SpaceFields + } + HasSpaceStub func() bool + hasSpaceMutex sync.RWMutex + hasSpaceArgsForCall []struct{} + hasSpaceReturns struct { + result1 bool + } + UsernameStub func() string + usernameMutex sync.RWMutex + usernameArgsForCall []struct{} + usernameReturns struct { + result1 string + } + UserGuidStub func() string + userGuidMutex sync.RWMutex + userGuidArgsForCall []struct{} + userGuidReturns struct { + result1 string + } + UserEmailStub func() string + userEmailMutex sync.RWMutex + userEmailArgsForCall []struct{} + userEmailReturns struct { + result1 string + } + IsLoggedInStub func() bool + isLoggedInMutex sync.RWMutex + isLoggedInArgsForCall []struct{} + isLoggedInReturns struct { + result1 bool + } + IsSSLDisabledStub func() bool + isSSLDisabledMutex sync.RWMutex + isSSLDisabledArgsForCall []struct{} + isSSLDisabledReturns struct { + result1 bool + } + AsyncTimeoutStub func() uint + asyncTimeoutMutex sync.RWMutex + asyncTimeoutArgsForCall []struct{} + asyncTimeoutReturns struct { + result1 uint + } + TraceStub func() string + traceMutex sync.RWMutex + traceArgsForCall []struct{} + traceReturns struct { + result1 string + } + ColorEnabledStub func() string + colorEnabledMutex sync.RWMutex + colorEnabledArgsForCall []struct{} + colorEnabledReturns struct { + result1 string + } + LocaleStub func() string + localeMutex sync.RWMutex + localeArgsForCall []struct{} + localeReturns struct { + result1 string + } + PluginsStub func() map[string]string + pluginsMutex sync.RWMutex + pluginsArgsForCall []struct{} + pluginsReturns struct { + result1 map[string]string + } + UserHomePathStub func() string + userHomePathMutex sync.RWMutex + userHomePathArgsForCall []struct{} + userHomePathReturns struct { + result1 string + } + ClearSessionStub func() + clearSessionMutex sync.RWMutex + clearSessionArgsForCall []struct{} + SetApiEndpointStub func(string) + setApiEndpointMutex sync.RWMutex + setApiEndpointArgsForCall []struct { + arg1 string + } + SetApiVersionStub func(string) + setApiVersionMutex sync.RWMutex + setApiVersionArgsForCall []struct { + arg1 string + } + SetAuthenticationEndpointStub func(string) + setAuthenticationEndpointMutex sync.RWMutex + setAuthenticationEndpointArgsForCall []struct { + arg1 string + } + SetLoggregatorEndpointStub func(string) + setLoggregatorEndpointMutex sync.RWMutex + setLoggregatorEndpointArgsForCall []struct { + arg1 string + } + SetUaaEndpointStub func(string) + setUaaEndpointMutex sync.RWMutex + setUaaEndpointArgsForCall []struct { + arg1 string + } + SetAccessTokenStub func(string) + setAccessTokenMutex sync.RWMutex + setAccessTokenArgsForCall []struct { + arg1 string + } + SetRefreshTokenStub func(string) + setRefreshTokenMutex sync.RWMutex + setRefreshTokenArgsForCall []struct { + arg1 string + } + SetOrganizationFieldsStub func(models.OrganizationFields) + setOrganizationFieldsMutex sync.RWMutex + setOrganizationFieldsArgsForCall []struct { + arg1 models.OrganizationFields + } + SetSpaceFieldsStub func(models.SpaceFields) + setSpaceFieldsMutex sync.RWMutex + setSpaceFieldsArgsForCall []struct { + arg1 models.SpaceFields + } + SetSSLDisabledStub func(bool) + setSSLDisabledMutex sync.RWMutex + setSSLDisabledArgsForCall []struct { + arg1 bool + } + SetAsyncTimeoutStub func(uint) + setAsyncTimeoutMutex sync.RWMutex + setAsyncTimeoutArgsForCall []struct { + arg1 uint + } + SetTraceStub func(string) + setTraceMutex sync.RWMutex + setTraceArgsForCall []struct { + arg1 string + } + SetColorEnabledStub func(string) + setColorEnabledMutex sync.RWMutex + setColorEnabledArgsForCall []struct { + arg1 string + } + SetLocaleStub func(string) + setLocaleMutex sync.RWMutex + setLocaleArgsForCall []struct { + arg1 string + } + SetPluginStub func(string, string) + setPluginMutex sync.RWMutex + setPluginArgsForCall []struct { + arg1 string + arg2 string + } + CloseStub func() + closeMutex sync.RWMutex + closeArgsForCall []struct{} +} + +func (fake *FakeRepository) ApiEndpoint() string { + fake.apiEndpointMutex.Lock() + defer fake.apiEndpointMutex.Unlock() + fake.apiEndpointArgsForCall = append(fake.apiEndpointArgsForCall, struct{}{}) + if fake.ApiEndpointStub != nil { + return fake.ApiEndpointStub() + } else { + return fake.apiEndpointReturns.result1 + } +} + +func (fake *FakeRepository) ApiEndpointCallCount() int { + fake.apiEndpointMutex.RLock() + defer fake.apiEndpointMutex.RUnlock() + return len(fake.apiEndpointArgsForCall) +} + +func (fake *FakeRepository) ApiEndpointReturns(result1 string) { + fake.apiEndpointReturns = struct { + result1 string + }{result1} +} + +func (fake *FakeRepository) ApiVersion() string { + fake.apiVersionMutex.Lock() + defer fake.apiVersionMutex.Unlock() + fake.apiVersionArgsForCall = append(fake.apiVersionArgsForCall, struct{}{}) + if fake.ApiVersionStub != nil { + return fake.ApiVersionStub() + } else { + return fake.apiVersionReturns.result1 + } +} + +func (fake *FakeRepository) ApiVersionCallCount() int { + fake.apiVersionMutex.RLock() + defer fake.apiVersionMutex.RUnlock() + return len(fake.apiVersionArgsForCall) +} + +func (fake *FakeRepository) ApiVersionReturns(result1 string) { + fake.apiVersionReturns = struct { + result1 string + }{result1} +} + +func (fake *FakeRepository) HasAPIEndpoint() bool { + fake.hasAPIEndpointMutex.Lock() + defer fake.hasAPIEndpointMutex.Unlock() + fake.hasAPIEndpointArgsForCall = append(fake.hasAPIEndpointArgsForCall, struct{}{}) + if fake.HasAPIEndpointStub != nil { + return fake.HasAPIEndpointStub() + } else { + return fake.hasAPIEndpointReturns.result1 + } +} + +func (fake *FakeRepository) HasAPIEndpointCallCount() int { + fake.hasAPIEndpointMutex.RLock() + defer fake.hasAPIEndpointMutex.RUnlock() + return len(fake.hasAPIEndpointArgsForCall) +} + +func (fake *FakeRepository) HasAPIEndpointReturns(result1 bool) { + fake.hasAPIEndpointReturns = struct { + result1 bool + }{result1} +} + +func (fake *FakeRepository) AuthenticationEndpoint() string { + fake.authenticationEndpointMutex.Lock() + defer fake.authenticationEndpointMutex.Unlock() + fake.authenticationEndpointArgsForCall = append(fake.authenticationEndpointArgsForCall, struct{}{}) + if fake.AuthenticationEndpointStub != nil { + return fake.AuthenticationEndpointStub() + } else { + return fake.authenticationEndpointReturns.result1 + } +} + +func (fake *FakeRepository) AuthenticationEndpointCallCount() int { + fake.authenticationEndpointMutex.RLock() + defer fake.authenticationEndpointMutex.RUnlock() + return len(fake.authenticationEndpointArgsForCall) +} + +func (fake *FakeRepository) AuthenticationEndpointReturns(result1 string) { + fake.authenticationEndpointReturns = struct { + result1 string + }{result1} +} + +func (fake *FakeRepository) LoggregatorEndpoint() string { + fake.loggregatorEndpointMutex.Lock() + defer fake.loggregatorEndpointMutex.Unlock() + fake.loggregatorEndpointArgsForCall = append(fake.loggregatorEndpointArgsForCall, struct{}{}) + if fake.LoggregatorEndpointStub != nil { + return fake.LoggregatorEndpointStub() + } else { + return fake.loggregatorEndpointReturns.result1 + } +} + +func (fake *FakeRepository) LoggregatorEndpointCallCount() int { + fake.loggregatorEndpointMutex.RLock() + defer fake.loggregatorEndpointMutex.RUnlock() + return len(fake.loggregatorEndpointArgsForCall) +} + +func (fake *FakeRepository) LoggregatorEndpointReturns(result1 string) { + fake.loggregatorEndpointReturns = struct { + result1 string + }{result1} +} + +func (fake *FakeRepository) UaaEndpoint() string { + fake.uaaEndpointMutex.Lock() + defer fake.uaaEndpointMutex.Unlock() + fake.uaaEndpointArgsForCall = append(fake.uaaEndpointArgsForCall, struct{}{}) + if fake.UaaEndpointStub != nil { + return fake.UaaEndpointStub() + } else { + return fake.uaaEndpointReturns.result1 + } +} + +func (fake *FakeRepository) UaaEndpointCallCount() int { + fake.uaaEndpointMutex.RLock() + defer fake.uaaEndpointMutex.RUnlock() + return len(fake.uaaEndpointArgsForCall) +} + +func (fake *FakeRepository) UaaEndpointReturns(result1 string) { + fake.uaaEndpointReturns = struct { + result1 string + }{result1} +} + +func (fake *FakeRepository) AccessToken() string { + fake.accessTokenMutex.Lock() + defer fake.accessTokenMutex.Unlock() + fake.accessTokenArgsForCall = append(fake.accessTokenArgsForCall, struct{}{}) + if fake.AccessTokenStub != nil { + return fake.AccessTokenStub() + } else { + return fake.accessTokenReturns.result1 + } +} + +func (fake *FakeRepository) AccessTokenCallCount() int { + fake.accessTokenMutex.RLock() + defer fake.accessTokenMutex.RUnlock() + return len(fake.accessTokenArgsForCall) +} + +func (fake *FakeRepository) AccessTokenReturns(result1 string) { + fake.accessTokenReturns = struct { + result1 string + }{result1} +} + +func (fake *FakeRepository) RefreshToken() string { + fake.refreshTokenMutex.Lock() + defer fake.refreshTokenMutex.Unlock() + fake.refreshTokenArgsForCall = append(fake.refreshTokenArgsForCall, struct{}{}) + if fake.RefreshTokenStub != nil { + return fake.RefreshTokenStub() + } else { + return fake.refreshTokenReturns.result1 + } +} + +func (fake *FakeRepository) RefreshTokenCallCount() int { + fake.refreshTokenMutex.RLock() + defer fake.refreshTokenMutex.RUnlock() + return len(fake.refreshTokenArgsForCall) +} + +func (fake *FakeRepository) RefreshTokenReturns(result1 string) { + fake.refreshTokenReturns = struct { + result1 string + }{result1} +} + +func (fake *FakeRepository) OrganizationFields() models.OrganizationFields { + fake.organizationFieldsMutex.Lock() + defer fake.organizationFieldsMutex.Unlock() + fake.organizationFieldsArgsForCall = append(fake.organizationFieldsArgsForCall, struct{}{}) + if fake.OrganizationFieldsStub != nil { + return fake.OrganizationFieldsStub() + } else { + return fake.organizationFieldsReturns.result1 + } +} + +func (fake *FakeRepository) OrganizationFieldsCallCount() int { + fake.organizationFieldsMutex.RLock() + defer fake.organizationFieldsMutex.RUnlock() + return len(fake.organizationFieldsArgsForCall) +} + +func (fake *FakeRepository) OrganizationFieldsReturns(result1 models.OrganizationFields) { + fake.organizationFieldsReturns = struct { + result1 models.OrganizationFields + }{result1} +} + +func (fake *FakeRepository) HasOrganization() bool { + fake.hasOrganizationMutex.Lock() + defer fake.hasOrganizationMutex.Unlock() + fake.hasOrganizationArgsForCall = append(fake.hasOrganizationArgsForCall, struct{}{}) + if fake.HasOrganizationStub != nil { + return fake.HasOrganizationStub() + } else { + return fake.hasOrganizationReturns.result1 + } +} + +func (fake *FakeRepository) HasOrganizationCallCount() int { + fake.hasOrganizationMutex.RLock() + defer fake.hasOrganizationMutex.RUnlock() + return len(fake.hasOrganizationArgsForCall) +} + +func (fake *FakeRepository) HasOrganizationReturns(result1 bool) { + fake.hasOrganizationReturns = struct { + result1 bool + }{result1} +} + +func (fake *FakeRepository) SpaceFields() models.SpaceFields { + fake.spaceFieldsMutex.Lock() + defer fake.spaceFieldsMutex.Unlock() + fake.spaceFieldsArgsForCall = append(fake.spaceFieldsArgsForCall, struct{}{}) + if fake.SpaceFieldsStub != nil { + return fake.SpaceFieldsStub() + } else { + return fake.spaceFieldsReturns.result1 + } +} + +func (fake *FakeRepository) SpaceFieldsCallCount() int { + fake.spaceFieldsMutex.RLock() + defer fake.spaceFieldsMutex.RUnlock() + return len(fake.spaceFieldsArgsForCall) +} + +func (fake *FakeRepository) SpaceFieldsReturns(result1 models.SpaceFields) { + fake.spaceFieldsReturns = struct { + result1 models.SpaceFields + }{result1} +} + +func (fake *FakeRepository) HasSpace() bool { + fake.hasSpaceMutex.Lock() + defer fake.hasSpaceMutex.Unlock() + fake.hasSpaceArgsForCall = append(fake.hasSpaceArgsForCall, struct{}{}) + if fake.HasSpaceStub != nil { + return fake.HasSpaceStub() + } else { + return fake.hasSpaceReturns.result1 + } +} + +func (fake *FakeRepository) HasSpaceCallCount() int { + fake.hasSpaceMutex.RLock() + defer fake.hasSpaceMutex.RUnlock() + return len(fake.hasSpaceArgsForCall) +} + +func (fake *FakeRepository) HasSpaceReturns(result1 bool) { + fake.hasSpaceReturns = struct { + result1 bool + }{result1} +} + +func (fake *FakeRepository) Username() string { + fake.usernameMutex.Lock() + defer fake.usernameMutex.Unlock() + fake.usernameArgsForCall = append(fake.usernameArgsForCall, struct{}{}) + if fake.UsernameStub != nil { + return fake.UsernameStub() + } else { + return fake.usernameReturns.result1 + } +} + +func (fake *FakeRepository) UsernameCallCount() int { + fake.usernameMutex.RLock() + defer fake.usernameMutex.RUnlock() + return len(fake.usernameArgsForCall) +} + +func (fake *FakeRepository) UsernameReturns(result1 string) { + fake.usernameReturns = struct { + result1 string + }{result1} +} + +func (fake *FakeRepository) UserGuid() string { + fake.userGuidMutex.Lock() + defer fake.userGuidMutex.Unlock() + fake.userGuidArgsForCall = append(fake.userGuidArgsForCall, struct{}{}) + if fake.UserGuidStub != nil { + return fake.UserGuidStub() + } else { + return fake.userGuidReturns.result1 + } +} + +func (fake *FakeRepository) UserGuidCallCount() int { + fake.userGuidMutex.RLock() + defer fake.userGuidMutex.RUnlock() + return len(fake.userGuidArgsForCall) +} + +func (fake *FakeRepository) UserGuidReturns(result1 string) { + fake.userGuidReturns = struct { + result1 string + }{result1} +} + +func (fake *FakeRepository) UserEmail() string { + fake.userEmailMutex.Lock() + defer fake.userEmailMutex.Unlock() + fake.userEmailArgsForCall = append(fake.userEmailArgsForCall, struct{}{}) + if fake.UserEmailStub != nil { + return fake.UserEmailStub() + } else { + return fake.userEmailReturns.result1 + } +} + +func (fake *FakeRepository) UserEmailCallCount() int { + fake.userEmailMutex.RLock() + defer fake.userEmailMutex.RUnlock() + return len(fake.userEmailArgsForCall) +} + +func (fake *FakeRepository) UserEmailReturns(result1 string) { + fake.userEmailReturns = struct { + result1 string + }{result1} +} + +func (fake *FakeRepository) IsLoggedIn() bool { + fake.isLoggedInMutex.Lock() + defer fake.isLoggedInMutex.Unlock() + fake.isLoggedInArgsForCall = append(fake.isLoggedInArgsForCall, struct{}{}) + if fake.IsLoggedInStub != nil { + return fake.IsLoggedInStub() + } else { + return fake.isLoggedInReturns.result1 + } +} + +func (fake *FakeRepository) IsLoggedInCallCount() int { + fake.isLoggedInMutex.RLock() + defer fake.isLoggedInMutex.RUnlock() + return len(fake.isLoggedInArgsForCall) +} + +func (fake *FakeRepository) IsLoggedInReturns(result1 bool) { + fake.isLoggedInReturns = struct { + result1 bool + }{result1} +} + +func (fake *FakeRepository) IsSSLDisabled() bool { + fake.isSSLDisabledMutex.Lock() + defer fake.isSSLDisabledMutex.Unlock() + fake.isSSLDisabledArgsForCall = append(fake.isSSLDisabledArgsForCall, struct{}{}) + if fake.IsSSLDisabledStub != nil { + return fake.IsSSLDisabledStub() + } else { + return fake.isSSLDisabledReturns.result1 + } +} + +func (fake *FakeRepository) IsSSLDisabledCallCount() int { + fake.isSSLDisabledMutex.RLock() + defer fake.isSSLDisabledMutex.RUnlock() + return len(fake.isSSLDisabledArgsForCall) +} + +func (fake *FakeRepository) IsSSLDisabledReturns(result1 bool) { + fake.isSSLDisabledReturns = struct { + result1 bool + }{result1} +} + +func (fake *FakeRepository) AsyncTimeout() uint { + fake.asyncTimeoutMutex.Lock() + defer fake.asyncTimeoutMutex.Unlock() + fake.asyncTimeoutArgsForCall = append(fake.asyncTimeoutArgsForCall, struct{}{}) + if fake.AsyncTimeoutStub != nil { + return fake.AsyncTimeoutStub() + } else { + return fake.asyncTimeoutReturns.result1 + } +} + +func (fake *FakeRepository) AsyncTimeoutCallCount() int { + fake.asyncTimeoutMutex.RLock() + defer fake.asyncTimeoutMutex.RUnlock() + return len(fake.asyncTimeoutArgsForCall) +} + +func (fake *FakeRepository) AsyncTimeoutReturns(result1 uint) { + fake.asyncTimeoutReturns = struct { + result1 uint + }{result1} +} + +func (fake *FakeRepository) Trace() string { + fake.traceMutex.Lock() + defer fake.traceMutex.Unlock() + fake.traceArgsForCall = append(fake.traceArgsForCall, struct{}{}) + if fake.TraceStub != nil { + return fake.TraceStub() + } else { + return fake.traceReturns.result1 + } +} + +func (fake *FakeRepository) TraceCallCount() int { + fake.traceMutex.RLock() + defer fake.traceMutex.RUnlock() + return len(fake.traceArgsForCall) +} + +func (fake *FakeRepository) TraceReturns(result1 string) { + fake.traceReturns = struct { + result1 string + }{result1} +} + +func (fake *FakeRepository) ColorEnabled() string { + fake.colorEnabledMutex.Lock() + defer fake.colorEnabledMutex.Unlock() + fake.colorEnabledArgsForCall = append(fake.colorEnabledArgsForCall, struct{}{}) + if fake.ColorEnabledStub != nil { + return fake.ColorEnabledStub() + } else { + return fake.colorEnabledReturns.result1 + } +} + +func (fake *FakeRepository) ColorEnabledCallCount() int { + fake.colorEnabledMutex.RLock() + defer fake.colorEnabledMutex.RUnlock() + return len(fake.colorEnabledArgsForCall) +} + +func (fake *FakeRepository) ColorEnabledReturns(result1 string) { + fake.colorEnabledReturns = struct { + result1 string + }{result1} +} + +func (fake *FakeRepository) Locale() string { + fake.localeMutex.Lock() + defer fake.localeMutex.Unlock() + fake.localeArgsForCall = append(fake.localeArgsForCall, struct{}{}) + if fake.LocaleStub != nil { + return fake.LocaleStub() + } else { + return fake.localeReturns.result1 + } +} + +func (fake *FakeRepository) LocaleCallCount() int { + fake.localeMutex.RLock() + defer fake.localeMutex.RUnlock() + return len(fake.localeArgsForCall) +} + +func (fake *FakeRepository) LocaleReturns(result1 string) { + fake.localeReturns = struct { + result1 string + }{result1} +} + +func (fake *FakeRepository) Plugins() map[string]string { + fake.pluginsMutex.Lock() + defer fake.pluginsMutex.Unlock() + fake.pluginsArgsForCall = append(fake.pluginsArgsForCall, struct{}{}) + if fake.PluginsStub != nil { + return fake.PluginsStub() + } else { + return fake.pluginsReturns.result1 + } +} + +func (fake *FakeRepository) PluginsCallCount() int { + fake.pluginsMutex.RLock() + defer fake.pluginsMutex.RUnlock() + return len(fake.pluginsArgsForCall) +} + +func (fake *FakeRepository) PluginsReturns(result1 map[string]string) { + fake.pluginsReturns = struct { + result1 map[string]string + }{result1} +} + +func (fake *FakeRepository) UserHomePath() string { + fake.userHomePathMutex.Lock() + defer fake.userHomePathMutex.Unlock() + fake.userHomePathArgsForCall = append(fake.userHomePathArgsForCall, struct{}{}) + if fake.UserHomePathStub != nil { + return fake.UserHomePathStub() + } else { + return fake.userHomePathReturns.result1 + } +} + +func (fake *FakeRepository) UserHomePathCallCount() int { + fake.userHomePathMutex.RLock() + defer fake.userHomePathMutex.RUnlock() + return len(fake.userHomePathArgsForCall) +} + +func (fake *FakeRepository) UserHomePathReturns(result1 string) { + fake.userHomePathReturns = struct { + result1 string + }{result1} +} + +func (fake *FakeRepository) ClearSession() { + fake.clearSessionMutex.Lock() + defer fake.clearSessionMutex.Unlock() + fake.clearSessionArgsForCall = append(fake.clearSessionArgsForCall, struct{}{}) + if fake.ClearSessionStub != nil { + fake.ClearSessionStub() + } +} + +func (fake *FakeRepository) ClearSessionCallCount() int { + fake.clearSessionMutex.RLock() + defer fake.clearSessionMutex.RUnlock() + return len(fake.clearSessionArgsForCall) +} + +func (fake *FakeRepository) SetApiEndpoint(arg1 string) { + fake.setApiEndpointMutex.Lock() + defer fake.setApiEndpointMutex.Unlock() + fake.setApiEndpointArgsForCall = append(fake.setApiEndpointArgsForCall, struct { + arg1 string + }{arg1}) + if fake.SetApiEndpointStub != nil { + fake.SetApiEndpointStub(arg1) + } +} + +func (fake *FakeRepository) SetApiEndpointCallCount() int { + fake.setApiEndpointMutex.RLock() + defer fake.setApiEndpointMutex.RUnlock() + return len(fake.setApiEndpointArgsForCall) +} + +func (fake *FakeRepository) SetApiEndpointArgsForCall(i int) string { + fake.setApiEndpointMutex.RLock() + defer fake.setApiEndpointMutex.RUnlock() + return fake.setApiEndpointArgsForCall[i].arg1 +} + +func (fake *FakeRepository) SetApiVersion(arg1 string) { + fake.setApiVersionMutex.Lock() + defer fake.setApiVersionMutex.Unlock() + fake.setApiVersionArgsForCall = append(fake.setApiVersionArgsForCall, struct { + arg1 string + }{arg1}) + if fake.SetApiVersionStub != nil { + fake.SetApiVersionStub(arg1) + } +} + +func (fake *FakeRepository) SetApiVersionCallCount() int { + fake.setApiVersionMutex.RLock() + defer fake.setApiVersionMutex.RUnlock() + return len(fake.setApiVersionArgsForCall) +} + +func (fake *FakeRepository) SetApiVersionArgsForCall(i int) string { + fake.setApiVersionMutex.RLock() + defer fake.setApiVersionMutex.RUnlock() + return fake.setApiVersionArgsForCall[i].arg1 +} + +func (fake *FakeRepository) SetAuthenticationEndpoint(arg1 string) { + fake.setAuthenticationEndpointMutex.Lock() + defer fake.setAuthenticationEndpointMutex.Unlock() + fake.setAuthenticationEndpointArgsForCall = append(fake.setAuthenticationEndpointArgsForCall, struct { + arg1 string + }{arg1}) + if fake.SetAuthenticationEndpointStub != nil { + fake.SetAuthenticationEndpointStub(arg1) + } +} + +func (fake *FakeRepository) SetAuthenticationEndpointCallCount() int { + fake.setAuthenticationEndpointMutex.RLock() + defer fake.setAuthenticationEndpointMutex.RUnlock() + return len(fake.setAuthenticationEndpointArgsForCall) +} + +func (fake *FakeRepository) SetAuthenticationEndpointArgsForCall(i int) string { + fake.setAuthenticationEndpointMutex.RLock() + defer fake.setAuthenticationEndpointMutex.RUnlock() + return fake.setAuthenticationEndpointArgsForCall[i].arg1 +} + +func (fake *FakeRepository) SetLoggregatorEndpoint(arg1 string) { + fake.setLoggregatorEndpointMutex.Lock() + defer fake.setLoggregatorEndpointMutex.Unlock() + fake.setLoggregatorEndpointArgsForCall = append(fake.setLoggregatorEndpointArgsForCall, struct { + arg1 string + }{arg1}) + if fake.SetLoggregatorEndpointStub != nil { + fake.SetLoggregatorEndpointStub(arg1) + } +} + +func (fake *FakeRepository) SetLoggregatorEndpointCallCount() int { + fake.setLoggregatorEndpointMutex.RLock() + defer fake.setLoggregatorEndpointMutex.RUnlock() + return len(fake.setLoggregatorEndpointArgsForCall) +} + +func (fake *FakeRepository) SetLoggregatorEndpointArgsForCall(i int) string { + fake.setLoggregatorEndpointMutex.RLock() + defer fake.setLoggregatorEndpointMutex.RUnlock() + return fake.setLoggregatorEndpointArgsForCall[i].arg1 +} + +func (fake *FakeRepository) SetUaaEndpoint(arg1 string) { + fake.setUaaEndpointMutex.Lock() + defer fake.setUaaEndpointMutex.Unlock() + fake.setUaaEndpointArgsForCall = append(fake.setUaaEndpointArgsForCall, struct { + arg1 string + }{arg1}) + if fake.SetUaaEndpointStub != nil { + fake.SetUaaEndpointStub(arg1) + } +} + +func (fake *FakeRepository) SetUaaEndpointCallCount() int { + fake.setUaaEndpointMutex.RLock() + defer fake.setUaaEndpointMutex.RUnlock() + return len(fake.setUaaEndpointArgsForCall) +} + +func (fake *FakeRepository) SetUaaEndpointArgsForCall(i int) string { + fake.setUaaEndpointMutex.RLock() + defer fake.setUaaEndpointMutex.RUnlock() + return fake.setUaaEndpointArgsForCall[i].arg1 +} + +func (fake *FakeRepository) SetAccessToken(arg1 string) { + fake.setAccessTokenMutex.Lock() + defer fake.setAccessTokenMutex.Unlock() + fake.setAccessTokenArgsForCall = append(fake.setAccessTokenArgsForCall, struct { + arg1 string + }{arg1}) + if fake.SetAccessTokenStub != nil { + fake.SetAccessTokenStub(arg1) + } +} + +func (fake *FakeRepository) SetAccessTokenCallCount() int { + fake.setAccessTokenMutex.RLock() + defer fake.setAccessTokenMutex.RUnlock() + return len(fake.setAccessTokenArgsForCall) +} + +func (fake *FakeRepository) SetAccessTokenArgsForCall(i int) string { + fake.setAccessTokenMutex.RLock() + defer fake.setAccessTokenMutex.RUnlock() + return fake.setAccessTokenArgsForCall[i].arg1 +} + +func (fake *FakeRepository) SetRefreshToken(arg1 string) { + fake.setRefreshTokenMutex.Lock() + defer fake.setRefreshTokenMutex.Unlock() + fake.setRefreshTokenArgsForCall = append(fake.setRefreshTokenArgsForCall, struct { + arg1 string + }{arg1}) + if fake.SetRefreshTokenStub != nil { + fake.SetRefreshTokenStub(arg1) + } +} + +func (fake *FakeRepository) SetRefreshTokenCallCount() int { + fake.setRefreshTokenMutex.RLock() + defer fake.setRefreshTokenMutex.RUnlock() + return len(fake.setRefreshTokenArgsForCall) +} + +func (fake *FakeRepository) SetRefreshTokenArgsForCall(i int) string { + fake.setRefreshTokenMutex.RLock() + defer fake.setRefreshTokenMutex.RUnlock() + return fake.setRefreshTokenArgsForCall[i].arg1 +} + +func (fake *FakeRepository) SetOrganizationFields(arg1 models.OrganizationFields) { + fake.setOrganizationFieldsMutex.Lock() + defer fake.setOrganizationFieldsMutex.Unlock() + fake.setOrganizationFieldsArgsForCall = append(fake.setOrganizationFieldsArgsForCall, struct { + arg1 models.OrganizationFields + }{arg1}) + if fake.SetOrganizationFieldsStub != nil { + fake.SetOrganizationFieldsStub(arg1) + } +} + +func (fake *FakeRepository) SetOrganizationFieldsCallCount() int { + fake.setOrganizationFieldsMutex.RLock() + defer fake.setOrganizationFieldsMutex.RUnlock() + return len(fake.setOrganizationFieldsArgsForCall) +} + +func (fake *FakeRepository) SetOrganizationFieldsArgsForCall(i int) models.OrganizationFields { + fake.setOrganizationFieldsMutex.RLock() + defer fake.setOrganizationFieldsMutex.RUnlock() + return fake.setOrganizationFieldsArgsForCall[i].arg1 +} + +func (fake *FakeRepository) SetSpaceFields(arg1 models.SpaceFields) { + fake.setSpaceFieldsMutex.Lock() + defer fake.setSpaceFieldsMutex.Unlock() + fake.setSpaceFieldsArgsForCall = append(fake.setSpaceFieldsArgsForCall, struct { + arg1 models.SpaceFields + }{arg1}) + if fake.SetSpaceFieldsStub != nil { + fake.SetSpaceFieldsStub(arg1) + } +} + +func (fake *FakeRepository) SetSpaceFieldsCallCount() int { + fake.setSpaceFieldsMutex.RLock() + defer fake.setSpaceFieldsMutex.RUnlock() + return len(fake.setSpaceFieldsArgsForCall) +} + +func (fake *FakeRepository) SetSpaceFieldsArgsForCall(i int) models.SpaceFields { + fake.setSpaceFieldsMutex.RLock() + defer fake.setSpaceFieldsMutex.RUnlock() + return fake.setSpaceFieldsArgsForCall[i].arg1 +} + +func (fake *FakeRepository) SetSSLDisabled(arg1 bool) { + fake.setSSLDisabledMutex.Lock() + defer fake.setSSLDisabledMutex.Unlock() + fake.setSSLDisabledArgsForCall = append(fake.setSSLDisabledArgsForCall, struct { + arg1 bool + }{arg1}) + if fake.SetSSLDisabledStub != nil { + fake.SetSSLDisabledStub(arg1) + } +} + +func (fake *FakeRepository) SetSSLDisabledCallCount() int { + fake.setSSLDisabledMutex.RLock() + defer fake.setSSLDisabledMutex.RUnlock() + return len(fake.setSSLDisabledArgsForCall) +} + +func (fake *FakeRepository) SetSSLDisabledArgsForCall(i int) bool { + fake.setSSLDisabledMutex.RLock() + defer fake.setSSLDisabledMutex.RUnlock() + return fake.setSSLDisabledArgsForCall[i].arg1 +} + +func (fake *FakeRepository) SetAsyncTimeout(arg1 uint) { + fake.setAsyncTimeoutMutex.Lock() + defer fake.setAsyncTimeoutMutex.Unlock() + fake.setAsyncTimeoutArgsForCall = append(fake.setAsyncTimeoutArgsForCall, struct { + arg1 uint + }{arg1}) + if fake.SetAsyncTimeoutStub != nil { + fake.SetAsyncTimeoutStub(arg1) + } +} + +func (fake *FakeRepository) SetAsyncTimeoutCallCount() int { + fake.setAsyncTimeoutMutex.RLock() + defer fake.setAsyncTimeoutMutex.RUnlock() + return len(fake.setAsyncTimeoutArgsForCall) +} + +func (fake *FakeRepository) SetAsyncTimeoutArgsForCall(i int) uint { + fake.setAsyncTimeoutMutex.RLock() + defer fake.setAsyncTimeoutMutex.RUnlock() + return fake.setAsyncTimeoutArgsForCall[i].arg1 +} + +func (fake *FakeRepository) SetTrace(arg1 string) { + fake.setTraceMutex.Lock() + defer fake.setTraceMutex.Unlock() + fake.setTraceArgsForCall = append(fake.setTraceArgsForCall, struct { + arg1 string + }{arg1}) + if fake.SetTraceStub != nil { + fake.SetTraceStub(arg1) + } +} + +func (fake *FakeRepository) SetTraceCallCount() int { + fake.setTraceMutex.RLock() + defer fake.setTraceMutex.RUnlock() + return len(fake.setTraceArgsForCall) +} + +func (fake *FakeRepository) SetTraceArgsForCall(i int) string { + fake.setTraceMutex.RLock() + defer fake.setTraceMutex.RUnlock() + return fake.setTraceArgsForCall[i].arg1 +} + +func (fake *FakeRepository) SetColorEnabled(arg1 string) { + fake.setColorEnabledMutex.Lock() + defer fake.setColorEnabledMutex.Unlock() + fake.setColorEnabledArgsForCall = append(fake.setColorEnabledArgsForCall, struct { + arg1 string + }{arg1}) + if fake.SetColorEnabledStub != nil { + fake.SetColorEnabledStub(arg1) + } +} + +func (fake *FakeRepository) SetColorEnabledCallCount() int { + fake.setColorEnabledMutex.RLock() + defer fake.setColorEnabledMutex.RUnlock() + return len(fake.setColorEnabledArgsForCall) +} + +func (fake *FakeRepository) SetColorEnabledArgsForCall(i int) string { + fake.setColorEnabledMutex.RLock() + defer fake.setColorEnabledMutex.RUnlock() + return fake.setColorEnabledArgsForCall[i].arg1 +} + +func (fake *FakeRepository) SetLocale(arg1 string) { + fake.setLocaleMutex.Lock() + defer fake.setLocaleMutex.Unlock() + fake.setLocaleArgsForCall = append(fake.setLocaleArgsForCall, struct { + arg1 string + }{arg1}) + if fake.SetLocaleStub != nil { + fake.SetLocaleStub(arg1) + } +} + +func (fake *FakeRepository) SetLocaleCallCount() int { + fake.setLocaleMutex.RLock() + defer fake.setLocaleMutex.RUnlock() + return len(fake.setLocaleArgsForCall) +} + +func (fake *FakeRepository) SetLocaleArgsForCall(i int) string { + fake.setLocaleMutex.RLock() + defer fake.setLocaleMutex.RUnlock() + return fake.setLocaleArgsForCall[i].arg1 +} + +func (fake *FakeRepository) SetPlugin(arg1 string, arg2 string) { + fake.setPluginMutex.Lock() + defer fake.setPluginMutex.Unlock() + fake.setPluginArgsForCall = append(fake.setPluginArgsForCall, struct { + arg1 string + arg2 string + }{arg1, arg2}) + if fake.SetPluginStub != nil { + fake.SetPluginStub(arg1, arg2) + } +} + +func (fake *FakeRepository) SetPluginCallCount() int { + fake.setPluginMutex.RLock() + defer fake.setPluginMutex.RUnlock() + return len(fake.setPluginArgsForCall) +} + +func (fake *FakeRepository) SetPluginArgsForCall(i int) (string, string) { + fake.setPluginMutex.RLock() + defer fake.setPluginMutex.RUnlock() + return fake.setPluginArgsForCall[i].arg1, fake.setPluginArgsForCall[i].arg2 +} + +func (fake *FakeRepository) Close() { + fake.closeMutex.Lock() + defer fake.closeMutex.Unlock() + fake.closeArgsForCall = append(fake.closeArgsForCall, struct{}{}) + if fake.CloseStub != nil { + fake.CloseStub() + } +} + +func (fake *FakeRepository) CloseCallCount() int { + fake.closeMutex.RLock() + defer fake.closeMutex.RUnlock() + return len(fake.closeArgsForCall) +} + +var _ Repository = new(FakeRepository) diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/configuration/plugin_config/fakes/fake_plugin_configuration.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/configuration/plugin_config/fakes/fake_plugin_configuration.go new file mode 100644 index 0000000..822ad26 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/configuration/plugin_config/fakes/fake_plugin_configuration.go @@ -0,0 +1,131 @@ +// This file was generated by counterfeiter +package fakes + +import ( + "sync" + + "github.com/cloudfoundry/cli/cf/configuration/plugin_config" +) + +type FakePluginConfiguration struct { + PluginsStub func() map[string]plugin_config.PluginMetadata + pluginsMutex sync.RWMutex + pluginsArgsForCall []struct{} + pluginsReturns struct { + result1 map[string]plugin_config.PluginMetadata + } + SetPluginStub func(string, plugin_config.PluginMetadata) + setPluginMutex sync.RWMutex + setPluginArgsForCall []struct { + arg1 string + arg2 plugin_config.PluginMetadata + } + GetPluginPathStub func() string + getPluginPathMutex sync.RWMutex + getPluginPathArgsForCall []struct{} + getPluginPathReturns struct { + result1 string + } + RemovePluginStub func(string) + removePluginMutex sync.RWMutex + removePluginArgsForCall []struct { + arg1 string + } +} + +func (fake *FakePluginConfiguration) Plugins() map[string]plugin_config.PluginMetadata { + fake.pluginsMutex.Lock() + fake.pluginsArgsForCall = append(fake.pluginsArgsForCall, struct{}{}) + fake.pluginsMutex.Unlock() + if fake.PluginsStub != nil { + return fake.PluginsStub() + } else { + return fake.pluginsReturns.result1 + } +} + +func (fake *FakePluginConfiguration) PluginsCallCount() int { + fake.pluginsMutex.RLock() + defer fake.pluginsMutex.RUnlock() + return len(fake.pluginsArgsForCall) +} + +func (fake *FakePluginConfiguration) PluginsReturns(result1 map[string]plugin_config.PluginMetadata) { + fake.PluginsStub = nil + fake.pluginsReturns = struct { + result1 map[string]plugin_config.PluginMetadata + }{result1} +} + +func (fake *FakePluginConfiguration) SetPlugin(arg1 string, arg2 plugin_config.PluginMetadata) { + fake.setPluginMutex.Lock() + fake.setPluginArgsForCall = append(fake.setPluginArgsForCall, struct { + arg1 string + arg2 plugin_config.PluginMetadata + }{arg1, arg2}) + fake.setPluginMutex.Unlock() + if fake.SetPluginStub != nil { + fake.SetPluginStub(arg1, arg2) + } +} + +func (fake *FakePluginConfiguration) SetPluginCallCount() int { + fake.setPluginMutex.RLock() + defer fake.setPluginMutex.RUnlock() + return len(fake.setPluginArgsForCall) +} + +func (fake *FakePluginConfiguration) SetPluginArgsForCall(i int) (string, plugin_config.PluginMetadata) { + fake.setPluginMutex.RLock() + defer fake.setPluginMutex.RUnlock() + return fake.setPluginArgsForCall[i].arg1, fake.setPluginArgsForCall[i].arg2 +} + +func (fake *FakePluginConfiguration) GetPluginPath() string { + fake.getPluginPathMutex.Lock() + fake.getPluginPathArgsForCall = append(fake.getPluginPathArgsForCall, struct{}{}) + fake.getPluginPathMutex.Unlock() + if fake.GetPluginPathStub != nil { + return fake.GetPluginPathStub() + } else { + return fake.getPluginPathReturns.result1 + } +} + +func (fake *FakePluginConfiguration) GetPluginPathCallCount() int { + fake.getPluginPathMutex.RLock() + defer fake.getPluginPathMutex.RUnlock() + return len(fake.getPluginPathArgsForCall) +} + +func (fake *FakePluginConfiguration) GetPluginPathReturns(result1 string) { + fake.GetPluginPathStub = nil + fake.getPluginPathReturns = struct { + result1 string + }{result1} +} + +func (fake *FakePluginConfiguration) RemovePlugin(arg1 string) { + fake.removePluginMutex.Lock() + fake.removePluginArgsForCall = append(fake.removePluginArgsForCall, struct { + arg1 string + }{arg1}) + fake.removePluginMutex.Unlock() + if fake.RemovePluginStub != nil { + fake.RemovePluginStub(arg1) + } +} + +func (fake *FakePluginConfiguration) RemovePluginCallCount() int { + fake.removePluginMutex.RLock() + defer fake.removePluginMutex.RUnlock() + return len(fake.removePluginArgsForCall) +} + +func (fake *FakePluginConfiguration) RemovePluginArgsForCall(i int) string { + fake.removePluginMutex.RLock() + defer fake.removePluginMutex.RUnlock() + return fake.removePluginArgsForCall[i].arg1 +} + +var _ plugin_config.PluginConfiguration = new(FakePluginConfiguration) diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/configuration/plugin_config/plugin_config.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/configuration/plugin_config/plugin_config.go new file mode 100644 index 0000000..bc0120b --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/configuration/plugin_config/plugin_config.go @@ -0,0 +1,99 @@ +package plugin_config + +import ( + "path/filepath" + "sync" + + "github.com/cloudfoundry/cli/cf/configuration" + "github.com/cloudfoundry/cli/cf/configuration/config_helpers" +) + +type PluginConfiguration interface { + Plugins() map[string]PluginMetadata + SetPlugin(string, PluginMetadata) + GetPluginPath() string + RemovePlugin(string) +} + +type PluginConfig struct { + mutex *sync.RWMutex + initOnce *sync.Once + persistor configuration.Persistor + onError func(error) + data *PluginData + pluginPath string +} + +func NewPluginConfig(errorHandler func(error)) *PluginConfig { + pluginPath := filepath.Join(config_helpers.PluginRepoDir(), ".cf", "plugins") + return &PluginConfig{ + data: NewData(), + mutex: new(sync.RWMutex), + initOnce: new(sync.Once), + persistor: configuration.NewDiskPersistor(filepath.Join(pluginPath, "config.json")), + onError: errorHandler, + pluginPath: pluginPath, + } +} + +/* getter methods */ +func (c *PluginConfig) GetPluginPath() string { + return c.pluginPath +} + +func (c *PluginConfig) Plugins() map[string]PluginMetadata { + c.read() + return c.data.Plugins +} + +/* setter methods */ +func (c *PluginConfig) SetPlugin(name string, metadata PluginMetadata) { + if c.data.Plugins == nil { + c.data.Plugins = make(map[string]PluginMetadata) + } + c.write(func() { + c.data.Plugins[name] = metadata + }) +} + +func (c *PluginConfig) RemovePlugin(name string) { + c.write(func() { + delete(c.data.Plugins, name) + }) +} + +/* Functions that handel locking */ +func (c *PluginConfig) init() { + //only read from disk if it was never read + c.initOnce.Do(func() { + err := c.persistor.Load(c.data) + if err != nil { + c.onError(err) + } + }) +} + +func (c *PluginConfig) read() { + c.mutex.RLock() + defer c.mutex.RUnlock() + c.init() +} + +func (c *PluginConfig) write(cb func()) { + c.mutex.Lock() + defer c.mutex.Unlock() + c.init() + + cb() + + err := c.persistor.Save(c.data) + if err != nil { + c.onError(err) + } +} + +// CLOSERS +func (c *PluginConfig) Close() { + c.read() + // perform a read to ensure write lock has been cleared +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/configuration/plugin_config/plugin_config_test.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/configuration/plugin_config/plugin_config_test.go new file mode 100644 index 0000000..df144c3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/configuration/plugin_config/plugin_config_test.go @@ -0,0 +1,134 @@ +package plugin_config_test + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/cloudfoundry/cli/cf/configuration/config_helpers" + . "github.com/cloudfoundry/cli/cf/configuration/plugin_config" + "github.com/cloudfoundry/cli/plugin" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("PluginConfig", func() { + var ( + metadata PluginMetadata + commands1 []plugin.Command + commands2 []plugin.Command + ) + + BeforeEach(func() { + commands1 = []plugin.Command{ + { + Name: "test_1_cmd1", + HelpText: "help text for test1 cmd1", + }, + { + Name: "test_1_cmd2", + HelpText: "help text for test1 cmd2", + }, + } + + commands2 = []plugin.Command{ + { + Name: "test_2_cmd1", + HelpText: "help text for test2 cmd1", + }, + { + Name: "test_2_cmd2", + HelpText: "help text for test2 cmd2", + }, + } + + metadata = PluginMetadata{ + Location: "../../../fixtures/plugins/test_1.exe", + Commands: commands1, + } + }) + + Describe("Reading configuration data", func() { + BeforeEach(func() { + + config_helpers.PluginRepoDir = func() string { + return filepath.Join("..", "..", "..", "fixtures", "config", "plugin-config") + } + }) + + It("returns a list of plugin executables and their location", func() { + pluginConfig := NewPluginConfig(func(err error) { + if err != nil { + panic(fmt.Sprintf("Config error: %s", err)) + } + }) + plugins := pluginConfig.Plugins() + + Expect(plugins["Test1"].Location).To(Equal("../../../fixtures/plugins/test_1.exe")) + Expect(plugins["Test1"].Commands).To(Equal(commands1)) + Expect(plugins["Test2"].Location).To(Equal("../../../fixtures/plugins/test_2.exe")) + Expect(plugins["Test2"].Commands).To(Equal(commands2)) + }) + }) + + Describe("Writing configuration data", func() { + BeforeEach(func() { + config_helpers.PluginRepoDir = func() string { return os.TempDir() } + }) + + AfterEach(func() { + os.Remove(filepath.Join(os.TempDir(), ".cf", "plugins", "config.json")) + }) + + It("saves plugin location and executable information", func() { + pluginConfig := NewPluginConfig(func(err error) { + if err != nil { + panic(fmt.Sprintf("Config error: %s", err)) + } + }) + + pluginConfig.SetPlugin("foo", metadata) + plugins := pluginConfig.Plugins() + Expect(plugins["foo"].Commands).To(Equal(commands1)) + }) + }) + + Describe("Removing configuration data", func() { + var ( + pluginConfig *PluginConfig + ) + + BeforeEach(func() { + config_helpers.PluginRepoDir = func() string { return os.TempDir() } + pluginConfig = NewPluginConfig(func(err error) { + if err != nil { + panic(fmt.Sprintf("Config error: %s", err)) + } + }) + }) + + AfterEach(func() { + os.Remove(filepath.Join(os.TempDir())) + }) + + It("removes plugin location and executable information", func() { + pluginConfig.SetPlugin("foo", metadata) + + plugins := pluginConfig.Plugins() + Expect(plugins).To(HaveKey("foo")) + + pluginConfig.RemovePlugin("foo") + + plugins = pluginConfig.Plugins() + Expect(plugins).NotTo(HaveKey("foo")) + }) + + It("handles when the config is not yet initialized", func() { + pluginConfig.RemovePlugin("foo") + + plugins := pluginConfig.Plugins() + Expect(plugins).NotTo(HaveKey("foo")) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/configuration/plugin_config/plugin_data.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/configuration/plugin_config/plugin_data.go new file mode 100644 index 0000000..b171013 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/configuration/plugin_config/plugin_data.go @@ -0,0 +1,31 @@ +package plugin_config + +import ( + "encoding/json" + + "github.com/cloudfoundry/cli/plugin" +) + +type PluginData struct { + Plugins map[string]PluginMetadata +} + +type PluginMetadata struct { + Location string + Version plugin.VersionType + Commands []plugin.Command +} + +func NewData() *PluginData { + return &PluginData{ + Plugins: make(map[string]PluginMetadata), + } +} + +func (pd *PluginData) JsonMarshalV3() (output []byte, err error) { + return json.MarshalIndent(pd, "", " ") +} + +func (pd *PluginData) JsonUnmarshalV3(input []byte) (err error) { + return json.Unmarshal(input, pd) +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/configuration/plugin_config/plugin_suite_test.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/configuration/plugin_config/plugin_suite_test.go new file mode 100644 index 0000000..288f24d --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/configuration/plugin_config/plugin_suite_test.go @@ -0,0 +1,13 @@ +package plugin_config_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestPlugins(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Plugins Suite") +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/app_event.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/app_event.go new file mode 100644 index 0000000..9bfa707 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/app_event.go @@ -0,0 +1,11 @@ +package models + +import "time" + +type EventFields struct { + Guid string + Name string + Timestamp time.Time + Description string + ActorName string +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/app_file.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/app_file.go new file mode 100644 index 0000000..34544a7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/app_file.go @@ -0,0 +1,7 @@ +package models + +type AppFileFields struct { + Path string + Sha1 string + Size int64 +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/app_instance.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/app_instance.go new file mode 100644 index 0000000..514c52d --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/app_instance.go @@ -0,0 +1,24 @@ +package models + +import "time" + +type InstanceState string + +const ( + InstanceStarting InstanceState = "starting" + InstanceRunning InstanceState = "running" + InstanceFlapping InstanceState = "flapping" + InstanceDown InstanceState = "down" + InstanceCrashed InstanceState = "crashed" +) + +type AppInstanceFields struct { + State InstanceState + Details string + Since time.Time + CpuUsage float64 // percentage + DiskQuota int64 // in bytes + DiskUsage int64 + MemQuota int64 + MemUsage int64 +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/application.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/application.go new file mode 100644 index 0000000..4a2f80a --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/application.go @@ -0,0 +1,157 @@ +package models + +import ( + "reflect" + "strings" + "time" +) + +type Application struct { + ApplicationFields + Stack *Stack + Routes []RouteSummary + Services []ServicePlanSummary +} + +func (model Application) HasRoute(route Route) bool { + for _, boundRoute := range model.Routes { + if boundRoute.Guid == route.Guid { + return true + } + } + return false +} + +func (model Application) ToParams() (params AppParams) { + state := strings.ToUpper(model.State) + params = AppParams{ + Guid: &model.Guid, + Name: &model.Name, + BuildpackUrl: &model.BuildpackUrl, + Command: &model.Command, + DiskQuota: &model.DiskQuota, + InstanceCount: &model.InstanceCount, + Memory: &model.Memory, + State: &state, + SpaceGuid: &model.SpaceGuid, + EnvironmentVars: &model.EnvironmentVars, + } + + if model.Stack != nil { + params.StackGuid = &model.Stack.Guid + } + + return +} + +type ApplicationFields struct { + Guid string + Name string + BuildpackUrl string + Command string + Diego bool + DetectedStartCommand string + DiskQuota int64 // in Megabytes + EnvironmentVars map[string]interface{} + InstanceCount int + Memory int64 // in Megabytes + RunningInstances int + HealthCheckTimeout int + State string + SpaceGuid string + PackageUpdatedAt *time.Time + PackageState string + StagingFailedReason string +} + +type AppParams struct { + BuildpackUrl *string + Command *string + DiskQuota *int64 + Domains *[]string + EnvironmentVars *map[string]interface{} + Guid *string + HealthCheckTimeout *int + Hosts *[]string + InstanceCount *int + Memory *int64 + Name *string + NoHostname bool + NoRoute bool + UseRandomHostname bool + Path *string + ServicesToBind *[]string + SpaceGuid *string + StackGuid *string + StackName *string + State *string +} + +func (app *AppParams) Merge(other *AppParams) { + if other.BuildpackUrl != nil { + app.BuildpackUrl = other.BuildpackUrl + } + if other.Command != nil { + app.Command = other.Command + } + if other.DiskQuota != nil { + app.DiskQuota = other.DiskQuota + } + if other.Domains != nil { + app.Domains = other.Domains + } + if other.EnvironmentVars != nil { + app.EnvironmentVars = other.EnvironmentVars + } + if other.Guid != nil { + app.Guid = other.Guid + } + if other.HealthCheckTimeout != nil { + app.HealthCheckTimeout = other.HealthCheckTimeout + } + if other.Hosts != nil { + app.Hosts = other.Hosts + } + if other.InstanceCount != nil { + app.InstanceCount = other.InstanceCount + } + if other.DiskQuota != nil { + app.DiskQuota = other.DiskQuota + } + if other.Memory != nil { + app.Memory = other.Memory + } + if other.Name != nil { + app.Name = other.Name + } + if other.Path != nil { + app.Path = other.Path + } + if other.ServicesToBind != nil { + app.ServicesToBind = other.ServicesToBind + } + if other.SpaceGuid != nil { + app.SpaceGuid = other.SpaceGuid + } + if other.StackGuid != nil { + app.StackGuid = other.StackGuid + } + if other.StackName != nil { + app.StackName = other.StackName + } + if other.State != nil { + app.State = other.State + } + + app.NoRoute = app.NoRoute || other.NoRoute + app.NoHostname = app.NoHostname || other.NoHostname + app.UseRandomHostname = app.UseRandomHostname || other.UseRandomHostname +} + +func (app *AppParams) IsEmpty() bool { + return reflect.DeepEqual(*app, AppParams{}) +} + +func (app *AppParams) IsHostEmpty() bool { + return app.Hosts == nil || len(*app.Hosts) == 0 +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/buildpack.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/buildpack.go new file mode 100644 index 0000000..f53416b --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/buildpack.go @@ -0,0 +1,11 @@ +package models + +type Buildpack struct { + Guid string + Name string + Position *int + Enabled *bool + Key string + Filename string + Locked *bool +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/domain.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/domain.go new file mode 100644 index 0000000..e7f6834 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/domain.go @@ -0,0 +1,17 @@ +package models + +import "fmt" + +type DomainFields struct { + Guid string + Name string + OwningOrganizationGuid string + Shared bool +} + +func (model DomainFields) UrlForHost(host string) string { + if host == "" { + return model.Name + } + return fmt.Sprintf("%s.%s", host, model.Name) +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/domain_test.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/domain_test.go new file mode 100644 index 0000000..8d137eb --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/domain_test.go @@ -0,0 +1,28 @@ +package models_test + +import ( + . "github.com/cloudfoundry/cli/cf/models" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("DomainFields", func() { + var route Route + + BeforeEach(func() { + route = Route{} + + domain := DomainFields{} + domain.Name = "example.com" + route.Domain = domain + }) + + It("uses the hostname as part of the URL", func() { + route.Host = "foo" + Expect(route.URL()).To(Equal("foo.example.com")) + }) + + It("omits the hostname when none is given", func() { + Expect(route.URL()).To(Equal("example.com")) + }) +}) diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/environment.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/environment.go new file mode 100644 index 0000000..48a6fe7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/environment.go @@ -0,0 +1,19 @@ +package models + +func NewEnvironment() *Environment { + return &Environment{ + System: make(map[string]interface{}), + Application: make(map[string]interface{}), + Environment: make(map[string]interface{}), + Running: make(map[string]interface{}), + Staging: make(map[string]interface{}), + } +} + +type Environment struct { + System map[string]interface{} `json:"system_env_json,omitempty"` + Environment map[string]interface{} `json:"environment_json,omitempty"` + Running map[string]interface{} `json:"running_env_json,omitempty"` + Staging map[string]interface{} `json:"staging_env_json,omitempty"` + Application map[string]interface{} `json:"application_env_json,omitempty"` +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/environment_variable.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/environment_variable.go new file mode 100644 index 0000000..77b9f86 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/environment_variable.go @@ -0,0 +1,12 @@ +package models + +func NewEnvironmentVariable(name string, value string) (e EnvironmentVariable) { + e.Name = name + e.Value = value + return +} + +type EnvironmentVariable struct { + Name string + Value string +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/feature_flag.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/feature_flag.go new file mode 100644 index 0000000..71722ba --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/feature_flag.go @@ -0,0 +1,14 @@ +package models + +func NewFeatureFlag(name string, enabled bool, errorMessage string) (f FeatureFlag) { + f.Name = name + f.Enabled = enabled + f.ErrorMessage = errorMessage + return +} + +type FeatureFlag struct { + Name string `json:"name"` + Enabled bool `json:"enabled"` + ErrorMessage string `json:"error_message"` +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/models_suite_test.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/models_suite_test.go new file mode 100644 index 0000000..2d4c6de --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/models_suite_test.go @@ -0,0 +1,13 @@ +package models_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestModels(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Models Suite") +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/organization.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/organization.go new file mode 100644 index 0000000..ac7d1e5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/organization.go @@ -0,0 +1,14 @@ +package models + +type OrganizationFields struct { + Guid string + Name string + QuotaDefinition QuotaFields +} + +type Organization struct { + OrganizationFields + Spaces []SpaceFields + Domains []DomainFields + SpaceQuotas []SpaceQuota +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/plugin_repo.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/plugin_repo.go new file mode 100644 index 0000000..25337df --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/plugin_repo.go @@ -0,0 +1,6 @@ +package models + +type PluginRepo struct { + Name string + Url string +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/quota.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/quota.go new file mode 100644 index 0000000..751a03d --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/quota.go @@ -0,0 +1,21 @@ +package models + +func NewQuotaFields(name string, memory int64, InstanceMemoryLimit int64, routes int, services int, nonbasicservices bool) (q QuotaFields) { + q.Name = name + q.MemoryLimit = memory + q.InstanceMemoryLimit = InstanceMemoryLimit + q.RoutesLimit = routes + q.ServicesLimit = services + q.NonBasicServicesAllowed = nonbasicservices + return +} + +type QuotaFields struct { + Guid string `json:"guid,omitempty"` + Name string `json:"name"` + MemoryLimit int64 `json:"memory_limit"` // in Megabytes + InstanceMemoryLimit int64 `json:"instance_memory_limit"` // in Megabytes + RoutesLimit int `json:"total_routes"` + ServicesLimit int `json:"total_services"` + NonBasicServicesAllowed bool `json:"non_basic_services_allowed"` +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/route.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/route.go new file mode 100644 index 0000000..a34760e --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/route.go @@ -0,0 +1,32 @@ +package models + +import "fmt" + +type Route struct { + Guid string + Host string + Domain DomainFields + + Space SpaceFields + Apps []ApplicationFields +} + +func (route Route) URL() string { + if route.Host == "" { + return route.Domain.Name + } + return fmt.Sprintf("%s.%s", route.Host, route.Domain.Name) +} + +type RouteSummary struct { + Guid string + Host string + Domain DomainFields +} + +func (model RouteSummary) URL() string { + if model.Host == "" { + return model.Domain.Name + } + return fmt.Sprintf("%s.%s", model.Host, model.Domain.Name) +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/security_group.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/security_group.go new file mode 100644 index 0000000..3f8c10f --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/security_group.go @@ -0,0 +1,21 @@ +package models + +// represents just the attributes for an security group +type SecurityGroupFields struct { + Name string + Guid string + Rules []map[string]interface{} +} + +// represents the JSON that we send up to CC when the user creates / updates a record +type SecurityGroupParams struct { + Name string `json:"name,omitempty"` + Guid string `json:"guid,omitempty"` + Rules []map[string]interface{} `json:"rules"` +} + +// represents a fully instantiated model returned by the CC (e.g.: with its attributes and the fields for its child objects) +type SecurityGroup struct { + SecurityGroupFields + Spaces []Space +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/service_auth_token.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/service_auth_token.go new file mode 100644 index 0000000..2df5c3b --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/service_auth_token.go @@ -0,0 +1,8 @@ +package models + +type ServiceAuthTokenFields struct { + Guid string + Label string + Provider string + Token string +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/service_binding.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/service_binding.go new file mode 100644 index 0000000..b30addd --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/service_binding.go @@ -0,0 +1,7 @@ +package models + +type ServiceBindingFields struct { + Guid string + Url string + AppGuid string +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/service_broker.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/service_broker.go new file mode 100644 index 0000000..dc7aa79 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/service_broker.go @@ -0,0 +1,10 @@ +package models + +type ServiceBroker struct { + Guid string + Name string + Username string + Password string + Url string + Services []ServiceOffering +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/service_instance.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/service_instance.go new file mode 100644 index 0000000..384e2de --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/service_instance.go @@ -0,0 +1,35 @@ +package models + +type LastOperationFields struct { + Type string + State string + Description string +} + +type ServiceInstanceRequest struct { + Name string `json:"name"` + SpaceGuid string `json:"space_guid"` + PlanGuid string `json:"service_plan_guid"` + Params map[string]interface{} `json:"parameters,omitempty"` +} + +type ServiceInstanceFields struct { + Guid string + Name string + LastOperation LastOperationFields + SysLogDrainUrl string + ApplicationNames []string + Params map[string]interface{} + DashboardUrl string +} + +type ServiceInstance struct { + ServiceInstanceFields + ServiceBindings []ServiceBindingFields + ServicePlan ServicePlanFields + ServiceOffering ServiceOfferingFields +} + +func (inst ServiceInstance) IsUserProvided() bool { + return inst.ServicePlan.Guid == "" +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/service_key.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/service_key.go new file mode 100644 index 0000000..1f1b395 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/service_key.go @@ -0,0 +1,14 @@ +package models + +type ServiceKeyFields struct { + Name string + Guid string + Url string + ServiceInstanceGuid string + ServiceInstanceUrl string +} + +type ServiceKey struct { + Fields ServiceKeyFields + Credentials map[string]interface{} +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/service_offering.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/service_offering.go new file mode 100644 index 0000000..658b718 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/service_offering.go @@ -0,0 +1,30 @@ +package models + +type ServiceOfferingFields struct { + Guid string + BrokerGuid string + Label string + Provider string + Version string + Description string + DocumentationUrl string +} + +type ServiceOffering struct { + ServiceOfferingFields + Plans []ServicePlanFields +} + +type ServiceOfferings []ServiceOffering + +func (s ServiceOfferings) Len() int { + return len(s) +} + +func (s ServiceOfferings) Swap(i, j int) { + s[i], s[j] = s[j], s[i] +} + +func (s ServiceOfferings) Less(i, j int) bool { + return s[i].Label < s[j].Label +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/service_plan.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/service_plan.go new file mode 100644 index 0000000..8d16d2f --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/service_plan.go @@ -0,0 +1,34 @@ +package models + +type ServicePlanFields struct { + Guid string + Name string + Free bool + Public bool + Description string + Active bool + ServiceOfferingGuid string + OrgNames []string +} + +type ServicePlan struct { + ServicePlanFields + ServiceOffering ServiceOfferingFields +} + +type ServicePlanSummary struct { + Guid string + Name string +} + +func (servicePlanFields ServicePlanFields) OrgHasVisibility(orgName string) bool { + if servicePlanFields.Public { + return true + } + for _, org := range servicePlanFields.OrgNames { + if org == orgName { + return true + } + } + return false +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/service_plan_test.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/service_plan_test.go new file mode 100644 index 0000000..64c047f --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/service_plan_test.go @@ -0,0 +1,47 @@ +package models_test + +import ( + . "github.com/cloudfoundry/cli/cf/models" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("ServicePlanFields", func() { + var servicePlanFields ServicePlanFields + + BeforeEach(func() { + servicePlanFields = ServicePlanFields{ + Guid: "I-am-a-guid", + Name: "BestServicePlanEver", + Free: false, + Public: true, + Description: "A Plan For Service", + Active: true, + ServiceOfferingGuid: "service-offering-guid", + OrgNames: []string{"org1", "org2"}, + } + }) + + Describe(".OrgHasVisibility", func() { + Context("when the service plan is public", func() { + It("returns true", func() { + Expect(servicePlanFields.OrgHasVisibility("anyOrg")).To(BeTrue()) + }) + }) + + Context("when the service plan is not public", func() { + BeforeEach(func() { + servicePlanFields.Public = false + }) + + It("returns true if the orgname is in the list of orgs that have visibility", func() { + Expect(servicePlanFields.OrgHasVisibility("org1")).To(BeTrue()) + }) + + It("returns false if the orgname is not in the list of orgs that have visibility", func() { + Expect(servicePlanFields.OrgHasVisibility("org-that-has-no-visibility")).To(BeFalse()) + }) + }) + + }) +}) diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/service_plan_visibility.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/service_plan_visibility.go new file mode 100644 index 0000000..61417e3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/service_plan_visibility.go @@ -0,0 +1,7 @@ +package models + +type ServicePlanVisibilityFields struct { + Guid string `json:"guid"` + ServicePlanGuid string `json:"service_plan_guid"` + OrganizationGuid string `json:"organization_guid"` +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/space.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/space.go new file mode 100644 index 0000000..e52e621 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/space.go @@ -0,0 +1,16 @@ +package models + +type SpaceFields struct { + Guid string + Name string +} + +type Space struct { + SpaceFields + Organization OrganizationFields + Applications []ApplicationFields + ServiceInstances []ServiceInstanceFields + Domains []DomainFields + SecurityGroups []SecurityGroupFields + SpaceQuotaGuid string +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/space_quota.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/space_quota.go new file mode 100644 index 0000000..02f15ea --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/space_quota.go @@ -0,0 +1,22 @@ +package models + +func NewSpaceQuota(name string, memory int64, routes int, services int, nonbasicservices bool, orgGuid string) (q SpaceQuota) { + q.Name = name + q.MemoryLimit = memory + q.RoutesLimit = routes + q.ServicesLimit = services + q.NonBasicServicesAllowed = nonbasicservices + q.OrgGuid = orgGuid + return +} + +type SpaceQuota struct { + Guid string `json:"guid,omitempty"` + Name string `json:"name"` + MemoryLimit int64 `json:"memory_limit"` // in Megabytes + InstanceMemoryLimit int64 `json:"instance_memory_limit"` // in Megabytes + RoutesLimit int `json:"total_routes"` + ServicesLimit int `json:"total_services"` + NonBasicServicesAllowed bool `json:"non_basic_services_allowed"` + OrgGuid string `json:"organization_guid"` +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/stack.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/stack.go new file mode 100644 index 0000000..c3b08a0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/stack.go @@ -0,0 +1,7 @@ +package models + +type Stack struct { + Guid string + Name string + Description string +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/user.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/user.go new file mode 100644 index 0000000..10ddaa0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/user.go @@ -0,0 +1,8 @@ +package models + +type UserFields struct { + Guid string + Username string + Password string + IsAdmin bool +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/user_provided_service.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/user_provided_service.go new file mode 100644 index 0000000..4c0d808 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/user_provided_service.go @@ -0,0 +1,17 @@ +package models + +type UserProvidedServiceSummary struct { + Total int `json:"total_results"` + Resources []UserProvidedServiceEntity `json:"resources"` +} + +type UserProvidedService struct { + Name string `json:"name,omitempty"` + Credentials map[string]interface{} `json:"credentials"` + SpaceGuid string `json:"space_guid,omitempty"` + SysLogDrainUrl string `json:"syslog_drain_url"` +} + +type UserProvidedServiceEntity struct { + UserProvidedService `json:"entity"` +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/user_roles.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/user_roles.go new file mode 100644 index 0000000..5413d28 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/cf/models/user_roles.go @@ -0,0 +1,29 @@ +package models + +const ( + ORG_USER = "OrgUser" + ORG_MANAGER = "OrgManager" + BILLING_MANAGER = "BillingManager" + ORG_AUDITOR = "OrgAuditor" + SPACE_MANAGER = "SpaceManager" + SPACE_DEVELOPER = "SpaceDeveloper" + SPACE_AUDITOR = "SpaceAuditor" +) + +var UserInputToOrgRole = map[string]string{ + "OrgManager": ORG_MANAGER, + "BillingManager": BILLING_MANAGER, + "OrgAuditor": ORG_AUDITOR, +} + +var UserInputToSpaceRole = map[string]string{ + "SpaceManager": SPACE_MANAGER, + "SpaceDeveloper": SPACE_DEVELOPER, + "SpaceAuditor": SPACE_AUDITOR, +} + +var SpaceRoleToUserInput = map[string]string{ + SPACE_MANAGER: "SpaceManager", + SPACE_DEVELOPER: "SpaceDeveloper", + SPACE_AUDITOR: "SpaceAuditor", +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/plugin/cli_connection.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/plugin/cli_connection.go new file mode 100644 index 0000000..66f7406 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/plugin/cli_connection.go @@ -0,0 +1,279 @@ +package plugin + +import ( + "errors" + "fmt" + "net" + "net/rpc" + "os" + "time" + + "github.com/cloudfoundry/cli/plugin/models" +) + +type cliConnection struct { + cliServerPort string +} + +func NewCliConnection(cliServerPort string) *cliConnection { + return &cliConnection{ + cliServerPort: cliServerPort, + } +} + +func (cliConnection *cliConnection) sendPluginMetadataToCliServer(metadata PluginMetadata) { + cliServerConn, err := rpc.Dial("tcp", "127.0.0.1:"+cliConnection.cliServerPort) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + defer cliServerConn.Close() + + var success bool + + err = cliServerConn.Call("CliRpcCmd.SetPluginMetadata", metadata, &success) + if err != nil { + fmt.Println(err) + os.Exit(1) + } + + if !success { + os.Exit(1) + } + + os.Exit(0) +} + +func (cliConnection *cliConnection) CliCommandWithoutTerminalOutput(args ...string) ([]string, error) { + return cliConnection.callCliCommand(true, args...) +} + +func (cliConnection *cliConnection) CliCommand(args ...string) ([]string, error) { + return cliConnection.callCliCommand(false, args...) +} + +func (cliConnection *cliConnection) callCliCommand(silently bool, args ...string) ([]string, error) { + client, err := rpc.Dial("tcp", "127.0.0.1:"+cliConnection.cliServerPort) + if err != nil { + return []string{}, err + } + defer client.Close() + + var success bool + + client.Call("CliRpcCmd.DisableTerminalOutput", silently, &success) + err = client.Call("CliRpcCmd.CallCoreCommand", args, &success) + + var cmdOutput []string + outputErr := client.Call("CliRpcCmd.GetOutputAndReset", success, &cmdOutput) + + if err != nil { + return cmdOutput, err + } else if !success { + return cmdOutput, errors.New("Error executing cli core command") + } + + if outputErr != nil { + return cmdOutput, errors.New("something completely unexpected happened") + } + return cmdOutput, nil +} + +func (cliConnection *cliConnection) pingCLI() { + //call back to cf saying we have been setup + var connErr error + var conn net.Conn + for i := 0; i < 5; i++ { + conn, connErr = net.Dial("tcp", "127.0.0.1:"+cliConnection.cliServerPort) + if connErr != nil { + time.Sleep(200 * time.Millisecond) + } else { + conn.Close() + break + } + } + if connErr != nil { + fmt.Println(connErr) + os.Exit(1) + } +} + +func (cliConnection *cliConnection) GetCurrentOrg() (plugin_models.Organization, error) { + client, err := rpc.Dial("tcp", "127.0.0.1:"+cliConnection.cliServerPort) + if err != nil { + return plugin_models.Organization{}, err + } + + var result plugin_models.Organization + + err = client.Call("CliRpcCmd.GetCurrentOrg", "", &result) + return result, err +} + +func (cliConnection *cliConnection) GetCurrentSpace() (plugin_models.Space, error) { + client, err := rpc.Dial("tcp", "127.0.0.1:"+cliConnection.cliServerPort) + if err != nil { + return plugin_models.Space{}, err + } + + var result plugin_models.Space + + err = client.Call("CliRpcCmd.GetCurrentSpace", "", &result) + return result, err +} + +func (cliConnection *cliConnection) Username() (string, error) { + client, err := rpc.Dial("tcp", "127.0.0.1:"+cliConnection.cliServerPort) + if err != nil { + return "", err + } + + var result string + + err = client.Call("CliRpcCmd.Username", "", &result) + return result, err +} + +func (cliConnection *cliConnection) UserGuid() (string, error) { + client, err := rpc.Dial("tcp", "127.0.0.1:"+cliConnection.cliServerPort) + if err != nil { + return "", err + } + + var result string + + err = client.Call("CliRpcCmd.UserGuid", "", &result) + return result, err +} + +func (cliConnection *cliConnection) UserEmail() (string, error) { + client, err := rpc.Dial("tcp", "127.0.0.1:"+cliConnection.cliServerPort) + if err != nil { + return "", err + } + + var result string + + err = client.Call("CliRpcCmd.UserEmail", "", &result) + return result, err +} + +func (cliConnection *cliConnection) IsSSLDisabled() (bool, error) { + client, err := rpc.Dial("tcp", "127.0.0.1:"+cliConnection.cliServerPort) + if err != nil { + return false, err + } + + var result bool + + err = client.Call("CliRpcCmd.IsSSLDisabled", "", &result) + return result, err +} + +func (cliConnection *cliConnection) IsLoggedIn() (bool, error) { + client, err := rpc.Dial("tcp", "127.0.0.1:"+cliConnection.cliServerPort) + if err != nil { + return false, err + } + + var result bool + + err = client.Call("CliRpcCmd.IsLoggedIn", "", &result) + return result, err +} + +func (cliConnection *cliConnection) HasOrganization() (bool, error) { + client, err := rpc.Dial("tcp", "127.0.0.1:"+cliConnection.cliServerPort) + if err != nil { + return false, err + } + + var result bool + + err = client.Call("CliRpcCmd.HasOrganization", "", &result) + return result, err +} + +func (cliConnection *cliConnection) HasSpace() (bool, error) { + client, err := rpc.Dial("tcp", "127.0.0.1:"+cliConnection.cliServerPort) + if err != nil { + return false, err + } + + var result bool + + err = client.Call("CliRpcCmd.HasSpace", "", &result) + return result, err +} + +func (cliConnection *cliConnection) ApiEndpoint() (string, error) { + client, err := rpc.Dial("tcp", "127.0.0.1:"+cliConnection.cliServerPort) + if err != nil { + return "", err + } + + var result string + + err = client.Call("CliRpcCmd.ApiEndpoint", "", &result) + return result, err +} + +func (cliConnection *cliConnection) HasAPIEndpoint() (bool, error) { + client, err := rpc.Dial("tcp", "127.0.0.1:"+cliConnection.cliServerPort) + if err != nil { + return false, err + } + + var result bool + + err = client.Call("CliRpcCmd.HasApiEndpoint", "", &result) + return result, err +} + +func (cliConnection *cliConnection) ApiVersion() (string, error) { + client, err := rpc.Dial("tcp", "127.0.0.1:"+cliConnection.cliServerPort) + if err != nil { + return "", err + } + + var result string + + err = client.Call("CliRpcCmd.ApiVersion", "", &result) + return result, err +} + +func (cliConnection *cliConnection) LoggregatorEndpoint() (string, error) { + client, err := rpc.Dial("tcp", "127.0.0.1:"+cliConnection.cliServerPort) + if err != nil { + return "", err + } + + var result string + + err = client.Call("CliRpcCmd.LoggregatorEndpoint", "", &result) + return result, err +} + +func (cliConnection *cliConnection) DopplerEndpoint() (string, error) { + client, err := rpc.Dial("tcp", "127.0.0.1:"+cliConnection.cliServerPort) + if err != nil { + return "", err + } + + var result string + + err = client.Call("CliRpcCmd.DopplerEndpoint", "", &result) + return result, err +} + +func (cliConnection *cliConnection) AccessToken() (string, error) { + client, err := rpc.Dial("tcp", "127.0.0.1:"+cliConnection.cliServerPort) + if err != nil { + return "", err + } + + var result string + + err = client.Call("CliRpcCmd.AccessToken", "", &result) + return result, err +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/plugin/fakes/fake_cli_connection.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/plugin/fakes/fake_cli_connection.go new file mode 100644 index 0000000..3faca1b --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/plugin/fakes/fake_cli_connection.go @@ -0,0 +1,578 @@ +// This file was generated by counterfeiter +package fakes + +import ( + "sync" + + "github.com/cloudfoundry/cli/plugin" + "github.com/cloudfoundry/cli/plugin/models" +) + +type FakeCliConnection struct { + CliCommandWithoutTerminalOutputStub func(args ...string) ([]string, error) + cliCommandWithoutTerminalOutputMutex sync.RWMutex + cliCommandWithoutTerminalOutputArgsForCall []struct { + args []string + } + cliCommandWithoutTerminalOutputReturns struct { + result1 []string + result2 error + } + CliCommandStub func(args ...string) ([]string, error) + cliCommandMutex sync.RWMutex + cliCommandArgsForCall []struct { + args []string + } + cliCommandReturns struct { + result1 []string + result2 error + } + GetCurrentOrgStub func() (plugin_models.Organization, error) + getCurrentOrgMutex sync.RWMutex + getCurrentOrgArgsForCall []struct{} + getCurrentOrgReturns struct { + result1 plugin_models.Organization + result2 error + } + GetCurrentSpaceStub func() (plugin_models.Space, error) + getCurrentSpaceMutex sync.RWMutex + getCurrentSpaceArgsForCall []struct{} + getCurrentSpaceReturns struct { + result1 plugin_models.Space + result2 error + } + UsernameStub func() (string, error) + usernameMutex sync.RWMutex + usernameArgsForCall []struct{} + usernameReturns struct { + result1 string + result2 error + } + UserGuidStub func() (string, error) + userGuidMutex sync.RWMutex + userGuidArgsForCall []struct{} + userGuidReturns struct { + result1 string + result2 error + } + UserEmailStub func() (string, error) + userEmailMutex sync.RWMutex + userEmailArgsForCall []struct{} + userEmailReturns struct { + result1 string + result2 error + } + IsLoggedInStub func() (bool, error) + isLoggedInMutex sync.RWMutex + isLoggedInArgsForCall []struct{} + isLoggedInReturns struct { + result1 bool + result2 error + } + IsSSLDisabledStub func() (bool, error) + isSSLDisabledMutex sync.RWMutex + isSSLDisabledArgsForCall []struct{} + isSSLDisabledReturns struct { + result1 bool + result2 error + } + HasOrganizationStub func() (bool, error) + hasOrganizationMutex sync.RWMutex + hasOrganizationArgsForCall []struct{} + hasOrganizationReturns struct { + result1 bool + result2 error + } + HasSpaceStub func() (bool, error) + hasSpaceMutex sync.RWMutex + hasSpaceArgsForCall []struct{} + hasSpaceReturns struct { + result1 bool + result2 error + } + ApiEndpointStub func() (string, error) + apiEndpointMutex sync.RWMutex + apiEndpointArgsForCall []struct{} + apiEndpointReturns struct { + result1 string + result2 error + } + ApiVersionStub func() (string, error) + apiVersionMutex sync.RWMutex + apiVersionArgsForCall []struct{} + apiVersionReturns struct { + result1 string + result2 error + } + HasAPIEndpointStub func() (bool, error) + hasAPIEndpointMutex sync.RWMutex + hasAPIEndpointArgsForCall []struct{} + hasAPIEndpointReturns struct { + result1 bool + result2 error + } + LoggregatorEndpointStub func() (string, error) + loggregatorEndpointMutex sync.RWMutex + loggregatorEndpointArgsForCall []struct{} + loggregatorEndpointReturns struct { + result1 string + result2 error + } + DopplerEndpointStub func() (string, error) + dopplerEndpointMutex sync.RWMutex + dopplerEndpointArgsForCall []struct{} + dopplerEndpointReturns struct { + result1 string + result2 error + } + AccessTokenStub func() (string, error) + accessTokenMutex sync.RWMutex + accessTokenArgsForCall []struct{} + accessTokenReturns struct { + result1 string + result2 error + } +} + +func (fake *FakeCliConnection) CliCommandWithoutTerminalOutput(args ...string) ([]string, error) { + fake.cliCommandWithoutTerminalOutputMutex.Lock() + fake.cliCommandWithoutTerminalOutputArgsForCall = append(fake.cliCommandWithoutTerminalOutputArgsForCall, struct { + args []string + }{args}) + fake.cliCommandWithoutTerminalOutputMutex.Unlock() + if fake.CliCommandWithoutTerminalOutputStub != nil { + return fake.CliCommandWithoutTerminalOutputStub(args...) + } else { + return fake.cliCommandWithoutTerminalOutputReturns.result1, fake.cliCommandWithoutTerminalOutputReturns.result2 + } +} + +func (fake *FakeCliConnection) CliCommandWithoutTerminalOutputCallCount() int { + fake.cliCommandWithoutTerminalOutputMutex.RLock() + defer fake.cliCommandWithoutTerminalOutputMutex.RUnlock() + return len(fake.cliCommandWithoutTerminalOutputArgsForCall) +} + +func (fake *FakeCliConnection) CliCommandWithoutTerminalOutputArgsForCall(i int) []string { + fake.cliCommandWithoutTerminalOutputMutex.RLock() + defer fake.cliCommandWithoutTerminalOutputMutex.RUnlock() + return fake.cliCommandWithoutTerminalOutputArgsForCall[i].args +} + +func (fake *FakeCliConnection) CliCommandWithoutTerminalOutputReturns(result1 []string, result2 error) { + fake.CliCommandWithoutTerminalOutputStub = nil + fake.cliCommandWithoutTerminalOutputReturns = struct { + result1 []string + result2 error + }{result1, result2} +} + +func (fake *FakeCliConnection) CliCommand(args ...string) ([]string, error) { + fake.cliCommandMutex.Lock() + fake.cliCommandArgsForCall = append(fake.cliCommandArgsForCall, struct { + args []string + }{args}) + fake.cliCommandMutex.Unlock() + if fake.CliCommandStub != nil { + return fake.CliCommandStub(args...) + } else { + return fake.cliCommandReturns.result1, fake.cliCommandReturns.result2 + } +} + +func (fake *FakeCliConnection) CliCommandCallCount() int { + fake.cliCommandMutex.RLock() + defer fake.cliCommandMutex.RUnlock() + return len(fake.cliCommandArgsForCall) +} + +func (fake *FakeCliConnection) CliCommandArgsForCall(i int) []string { + fake.cliCommandMutex.RLock() + defer fake.cliCommandMutex.RUnlock() + return fake.cliCommandArgsForCall[i].args +} + +func (fake *FakeCliConnection) CliCommandReturns(result1 []string, result2 error) { + fake.CliCommandStub = nil + fake.cliCommandReturns = struct { + result1 []string + result2 error + }{result1, result2} +} + +func (fake *FakeCliConnection) GetCurrentOrg() (plugin_models.Organization, error) { + fake.getCurrentOrgMutex.Lock() + fake.getCurrentOrgArgsForCall = append(fake.getCurrentOrgArgsForCall, struct{}{}) + fake.getCurrentOrgMutex.Unlock() + if fake.GetCurrentOrgStub != nil { + return fake.GetCurrentOrgStub() + } else { + return fake.getCurrentOrgReturns.result1, fake.getCurrentOrgReturns.result2 + } +} + +func (fake *FakeCliConnection) GetCurrentOrgCallCount() int { + fake.getCurrentOrgMutex.RLock() + defer fake.getCurrentOrgMutex.RUnlock() + return len(fake.getCurrentOrgArgsForCall) +} + +func (fake *FakeCliConnection) GetCurrentOrgReturns(result1 plugin_models.Organization, result2 error) { + fake.GetCurrentOrgStub = nil + fake.getCurrentOrgReturns = struct { + result1 plugin_models.Organization + result2 error + }{result1, result2} +} + +func (fake *FakeCliConnection) GetCurrentSpace() (plugin_models.Space, error) { + fake.getCurrentSpaceMutex.Lock() + fake.getCurrentSpaceArgsForCall = append(fake.getCurrentSpaceArgsForCall, struct{}{}) + fake.getCurrentSpaceMutex.Unlock() + if fake.GetCurrentSpaceStub != nil { + return fake.GetCurrentSpaceStub() + } else { + return fake.getCurrentSpaceReturns.result1, fake.getCurrentSpaceReturns.result2 + } +} + +func (fake *FakeCliConnection) GetCurrentSpaceCallCount() int { + fake.getCurrentSpaceMutex.RLock() + defer fake.getCurrentSpaceMutex.RUnlock() + return len(fake.getCurrentSpaceArgsForCall) +} + +func (fake *FakeCliConnection) GetCurrentSpaceReturns(result1 plugin_models.Space, result2 error) { + fake.GetCurrentSpaceStub = nil + fake.getCurrentSpaceReturns = struct { + result1 plugin_models.Space + result2 error + }{result1, result2} +} + +func (fake *FakeCliConnection) Username() (string, error) { + fake.usernameMutex.Lock() + fake.usernameArgsForCall = append(fake.usernameArgsForCall, struct{}{}) + fake.usernameMutex.Unlock() + if fake.UsernameStub != nil { + return fake.UsernameStub() + } else { + return fake.usernameReturns.result1, fake.usernameReturns.result2 + } +} + +func (fake *FakeCliConnection) UsernameCallCount() int { + fake.usernameMutex.RLock() + defer fake.usernameMutex.RUnlock() + return len(fake.usernameArgsForCall) +} + +func (fake *FakeCliConnection) UsernameReturns(result1 string, result2 error) { + fake.UsernameStub = nil + fake.usernameReturns = struct { + result1 string + result2 error + }{result1, result2} +} + +func (fake *FakeCliConnection) UserGuid() (string, error) { + fake.userGuidMutex.Lock() + fake.userGuidArgsForCall = append(fake.userGuidArgsForCall, struct{}{}) + fake.userGuidMutex.Unlock() + if fake.UserGuidStub != nil { + return fake.UserGuidStub() + } else { + return fake.userGuidReturns.result1, fake.userGuidReturns.result2 + } +} + +func (fake *FakeCliConnection) UserGuidCallCount() int { + fake.userGuidMutex.RLock() + defer fake.userGuidMutex.RUnlock() + return len(fake.userGuidArgsForCall) +} + +func (fake *FakeCliConnection) UserGuidReturns(result1 string, result2 error) { + fake.UserGuidStub = nil + fake.userGuidReturns = struct { + result1 string + result2 error + }{result1, result2} +} + +func (fake *FakeCliConnection) UserEmail() (string, error) { + fake.userEmailMutex.Lock() + fake.userEmailArgsForCall = append(fake.userEmailArgsForCall, struct{}{}) + fake.userEmailMutex.Unlock() + if fake.UserEmailStub != nil { + return fake.UserEmailStub() + } else { + return fake.userEmailReturns.result1, fake.userEmailReturns.result2 + } +} + +func (fake *FakeCliConnection) UserEmailCallCount() int { + fake.userEmailMutex.RLock() + defer fake.userEmailMutex.RUnlock() + return len(fake.userEmailArgsForCall) +} + +func (fake *FakeCliConnection) UserEmailReturns(result1 string, result2 error) { + fake.UserEmailStub = nil + fake.userEmailReturns = struct { + result1 string + result2 error + }{result1, result2} +} + +func (fake *FakeCliConnection) IsLoggedIn() (bool, error) { + fake.isLoggedInMutex.Lock() + fake.isLoggedInArgsForCall = append(fake.isLoggedInArgsForCall, struct{}{}) + fake.isLoggedInMutex.Unlock() + if fake.IsLoggedInStub != nil { + return fake.IsLoggedInStub() + } else { + return fake.isLoggedInReturns.result1, fake.isLoggedInReturns.result2 + } +} + +func (fake *FakeCliConnection) IsLoggedInCallCount() int { + fake.isLoggedInMutex.RLock() + defer fake.isLoggedInMutex.RUnlock() + return len(fake.isLoggedInArgsForCall) +} + +func (fake *FakeCliConnection) IsLoggedInReturns(result1 bool, result2 error) { + fake.IsLoggedInStub = nil + fake.isLoggedInReturns = struct { + result1 bool + result2 error + }{result1, result2} +} + +func (fake *FakeCliConnection) IsSSLDisabled() (bool, error) { + fake.isSSLDisabledMutex.Lock() + fake.isSSLDisabledArgsForCall = append(fake.isSSLDisabledArgsForCall, struct{}{}) + fake.isSSLDisabledMutex.Unlock() + if fake.IsSSLDisabledStub != nil { + return fake.IsSSLDisabledStub() + } else { + return fake.isSSLDisabledReturns.result1, fake.isSSLDisabledReturns.result2 + } +} + +func (fake *FakeCliConnection) IsSSLDisabledCallCount() int { + fake.isSSLDisabledMutex.RLock() + defer fake.isSSLDisabledMutex.RUnlock() + return len(fake.isSSLDisabledArgsForCall) +} + +func (fake *FakeCliConnection) IsSSLDisabledReturns(result1 bool, result2 error) { + fake.IsSSLDisabledStub = nil + fake.isSSLDisabledReturns = struct { + result1 bool + result2 error + }{result1, result2} +} + +func (fake *FakeCliConnection) HasOrganization() (bool, error) { + fake.hasOrganizationMutex.Lock() + fake.hasOrganizationArgsForCall = append(fake.hasOrganizationArgsForCall, struct{}{}) + fake.hasOrganizationMutex.Unlock() + if fake.HasOrganizationStub != nil { + return fake.HasOrganizationStub() + } else { + return fake.hasOrganizationReturns.result1, fake.hasOrganizationReturns.result2 + } +} + +func (fake *FakeCliConnection) HasOrganizationCallCount() int { + fake.hasOrganizationMutex.RLock() + defer fake.hasOrganizationMutex.RUnlock() + return len(fake.hasOrganizationArgsForCall) +} + +func (fake *FakeCliConnection) HasOrganizationReturns(result1 bool, result2 error) { + fake.HasOrganizationStub = nil + fake.hasOrganizationReturns = struct { + result1 bool + result2 error + }{result1, result2} +} + +func (fake *FakeCliConnection) HasSpace() (bool, error) { + fake.hasSpaceMutex.Lock() + fake.hasSpaceArgsForCall = append(fake.hasSpaceArgsForCall, struct{}{}) + fake.hasSpaceMutex.Unlock() + if fake.HasSpaceStub != nil { + return fake.HasSpaceStub() + } else { + return fake.hasSpaceReturns.result1, fake.hasSpaceReturns.result2 + } +} + +func (fake *FakeCliConnection) HasSpaceCallCount() int { + fake.hasSpaceMutex.RLock() + defer fake.hasSpaceMutex.RUnlock() + return len(fake.hasSpaceArgsForCall) +} + +func (fake *FakeCliConnection) HasSpaceReturns(result1 bool, result2 error) { + fake.HasSpaceStub = nil + fake.hasSpaceReturns = struct { + result1 bool + result2 error + }{result1, result2} +} + +func (fake *FakeCliConnection) ApiEndpoint() (string, error) { + fake.apiEndpointMutex.Lock() + fake.apiEndpointArgsForCall = append(fake.apiEndpointArgsForCall, struct{}{}) + fake.apiEndpointMutex.Unlock() + if fake.ApiEndpointStub != nil { + return fake.ApiEndpointStub() + } else { + return fake.apiEndpointReturns.result1, fake.apiEndpointReturns.result2 + } +} + +func (fake *FakeCliConnection) ApiEndpointCallCount() int { + fake.apiEndpointMutex.RLock() + defer fake.apiEndpointMutex.RUnlock() + return len(fake.apiEndpointArgsForCall) +} + +func (fake *FakeCliConnection) ApiEndpointReturns(result1 string, result2 error) { + fake.ApiEndpointStub = nil + fake.apiEndpointReturns = struct { + result1 string + result2 error + }{result1, result2} +} + +func (fake *FakeCliConnection) ApiVersion() (string, error) { + fake.apiVersionMutex.Lock() + fake.apiVersionArgsForCall = append(fake.apiVersionArgsForCall, struct{}{}) + fake.apiVersionMutex.Unlock() + if fake.ApiVersionStub != nil { + return fake.ApiVersionStub() + } else { + return fake.apiVersionReturns.result1, fake.apiVersionReturns.result2 + } +} + +func (fake *FakeCliConnection) ApiVersionCallCount() int { + fake.apiVersionMutex.RLock() + defer fake.apiVersionMutex.RUnlock() + return len(fake.apiVersionArgsForCall) +} + +func (fake *FakeCliConnection) ApiVersionReturns(result1 string, result2 error) { + fake.ApiVersionStub = nil + fake.apiVersionReturns = struct { + result1 string + result2 error + }{result1, result2} +} + +func (fake *FakeCliConnection) HasAPIEndpoint() (bool, error) { + fake.hasAPIEndpointMutex.Lock() + fake.hasAPIEndpointArgsForCall = append(fake.hasAPIEndpointArgsForCall, struct{}{}) + fake.hasAPIEndpointMutex.Unlock() + if fake.HasAPIEndpointStub != nil { + return fake.HasAPIEndpointStub() + } else { + return fake.hasAPIEndpointReturns.result1, fake.hasAPIEndpointReturns.result2 + } +} + +func (fake *FakeCliConnection) HasAPIEndpointCallCount() int { + fake.hasAPIEndpointMutex.RLock() + defer fake.hasAPIEndpointMutex.RUnlock() + return len(fake.hasAPIEndpointArgsForCall) +} + +func (fake *FakeCliConnection) HasAPIEndpointReturns(result1 bool, result2 error) { + fake.HasAPIEndpointStub = nil + fake.hasAPIEndpointReturns = struct { + result1 bool + result2 error + }{result1, result2} +} + +func (fake *FakeCliConnection) LoggregatorEndpoint() (string, error) { + fake.loggregatorEndpointMutex.Lock() + fake.loggregatorEndpointArgsForCall = append(fake.loggregatorEndpointArgsForCall, struct{}{}) + fake.loggregatorEndpointMutex.Unlock() + if fake.LoggregatorEndpointStub != nil { + return fake.LoggregatorEndpointStub() + } else { + return fake.loggregatorEndpointReturns.result1, fake.loggregatorEndpointReturns.result2 + } +} + +func (fake *FakeCliConnection) LoggregatorEndpointCallCount() int { + fake.loggregatorEndpointMutex.RLock() + defer fake.loggregatorEndpointMutex.RUnlock() + return len(fake.loggregatorEndpointArgsForCall) +} + +func (fake *FakeCliConnection) LoggregatorEndpointReturns(result1 string, result2 error) { + fake.LoggregatorEndpointStub = nil + fake.loggregatorEndpointReturns = struct { + result1 string + result2 error + }{result1, result2} +} + +func (fake *FakeCliConnection) DopplerEndpoint() (string, error) { + fake.dopplerEndpointMutex.Lock() + fake.dopplerEndpointArgsForCall = append(fake.dopplerEndpointArgsForCall, struct{}{}) + fake.dopplerEndpointMutex.Unlock() + if fake.DopplerEndpointStub != nil { + return fake.DopplerEndpointStub() + } else { + return fake.dopplerEndpointReturns.result1, fake.dopplerEndpointReturns.result2 + } +} + +func (fake *FakeCliConnection) DopplerEndpointCallCount() int { + fake.dopplerEndpointMutex.RLock() + defer fake.dopplerEndpointMutex.RUnlock() + return len(fake.dopplerEndpointArgsForCall) +} + +func (fake *FakeCliConnection) DopplerEndpointReturns(result1 string, result2 error) { + fake.DopplerEndpointStub = nil + fake.dopplerEndpointReturns = struct { + result1 string + result2 error + }{result1, result2} +} + +func (fake *FakeCliConnection) AccessToken() (string, error) { + fake.accessTokenMutex.Lock() + fake.accessTokenArgsForCall = append(fake.accessTokenArgsForCall, struct{}{}) + fake.accessTokenMutex.Unlock() + if fake.AccessTokenStub != nil { + return fake.AccessTokenStub() + } else { + return fake.accessTokenReturns.result1, fake.accessTokenReturns.result2 + } +} + +func (fake *FakeCliConnection) AccessTokenCallCount() int { + fake.accessTokenMutex.RLock() + defer fake.accessTokenMutex.RUnlock() + return len(fake.accessTokenArgsForCall) +} + +func (fake *FakeCliConnection) AccessTokenReturns(result1 string, result2 error) { + fake.AccessTokenStub = nil + fake.accessTokenReturns = struct { + result1 string + result2 error + }{result1, result2} +} + +var _ plugin.CliConnection = new(FakeCliConnection) diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/plugin/models/organization.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/plugin/models/organization.go new file mode 100644 index 0000000..1b109e8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/plugin/models/organization.go @@ -0,0 +1,11 @@ +package plugin_models + +type OrganizationFields struct { + Guid string + Name string + QuotaDefinition QuotaFields +} + +type Organization struct { + OrganizationFields +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/plugin/models/quotas.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/plugin/models/quotas.go new file mode 100644 index 0000000..ecb0973 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/plugin/models/quotas.go @@ -0,0 +1,11 @@ +package plugin_models + +type QuotaFields struct { + Guid string + Name string + MemoryLimit int64 + InstanceMemoryLimit int64 + RoutesLimit int + ServicesLimit int + NonBasicServicesAllowed bool +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/plugin/models/space.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/plugin/models/space.go new file mode 100644 index 0000000..87968f1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/plugin/models/space.go @@ -0,0 +1,10 @@ +package plugin_models + +type SpaceFields struct { + Guid string + Name string +} + +type Space struct { + SpaceFields +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/plugin/plugin.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/plugin/plugin.go new file mode 100644 index 0000000..777241c --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/plugin/plugin.go @@ -0,0 +1,58 @@ +package plugin + +import "github.com/cloudfoundry/cli/plugin/models" + +/** + Command interface needs to be implemented for a runnable plugin of `cf` +**/ +type Plugin interface { + Run(cliConnection CliConnection, args []string) + GetMetadata() PluginMetadata +} + +/** + List of commands avaiable to CliConnection variable passed into run +**/ +type CliConnection interface { + CliCommandWithoutTerminalOutput(args ...string) ([]string, error) + CliCommand(args ...string) ([]string, error) + GetCurrentOrg() (plugin_models.Organization, error) + GetCurrentSpace() (plugin_models.Space, error) + Username() (string, error) + UserGuid() (string, error) + UserEmail() (string, error) + IsLoggedIn() (bool, error) + IsSSLDisabled() (bool, error) + HasOrganization() (bool, error) + HasSpace() (bool, error) + ApiEndpoint() (string, error) + ApiVersion() (string, error) + HasAPIEndpoint() (bool, error) + LoggregatorEndpoint() (string, error) + DopplerEndpoint() (string, error) + AccessToken() (string, error) +} + +type VersionType struct { + Major int + Minor int + Build int +} + +type PluginMetadata struct { + Name string + Version VersionType + Commands []Command +} + +type Usage struct { + Usage string + Options map[string]string +} + +type Command struct { + Name string + Alias string + HelpText string + UsageDetails Usage //Detail usage to be displayed in `cf help ` +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/plugin/plugin_shim.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/plugin/plugin_shim.go new file mode 100644 index 0000000..87be99c --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/plugin/plugin_shim.go @@ -0,0 +1,24 @@ +package plugin + +import "os" + +/** + * This function is called by the plugin to setup their server. This allows us to call Run on the plugin + * os.Args[1] port CF_CLI rpc server is running on + * os.Args[2] **OPTIONAL** + * SendMetadata - used to fetch the plugin metadata +**/ +func Start(cmd Plugin) { + cliConnection := NewCliConnection(os.Args[1]) + + cliConnection.pingCLI() + if isMetadataRequest(os.Args) { + cliConnection.sendPluginMetadataToCliServer(cmd.GetMetadata()) + } else { + cmd.Run(cliConnection, os.Args[2:]) + } +} + +func isMetadataRequest(args []string) bool { + return len(args) == 3 && args[2] == "SendMetadata" +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/plugin/plugin_shim_test.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/plugin/plugin_shim_test.go new file mode 100644 index 0000000..910af90 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/plugin/plugin_shim_test.go @@ -0,0 +1,25 @@ +package plugin_test + +import ( + "os/exec" + "path/filepath" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/onsi/gomega/gexec" +) + +var _ = Describe("Command", func() { + var ( + validPluginPath = filepath.Join("..", "fixtures", "plugins", "test_1.exe") + ) + + Describe(".Start", func() { + It("Exits with status 1 if it cannot ping the host port passed as an argument", func() { + args := []string{"0", "0"} + session, err := Start(exec.Command(validPluginPath, args...), GinkgoWriter, GinkgoWriter) + Expect(err).ToNot(HaveOccurred()) + Eventually(session, 2).Should(Exit(1)) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/plugin/plugin_suite_test.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/plugin/plugin_suite_test.go new file mode 100644 index 0000000..3a799a4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/plugin/plugin_suite_test.go @@ -0,0 +1,17 @@ +package plugin_test + +import ( + "path/filepath" + + "github.com/cloudfoundry/cli/testhelpers/plugin_builder" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestPlugin(t *testing.T) { + RegisterFailHandler(Fail) + plugin_builder.BuildTestBinary(filepath.Join("..", "fixtures", "plugins"), "test_1") + RunSpecs(t, "Plugin Suite") +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/plugin/rpc/cli_rpc_server.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/plugin/rpc/cli_rpc_server.go new file mode 100644 index 0000000..f5ef3aa --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/plugin/rpc/cli_rpc_server.go @@ -0,0 +1,222 @@ +package rpc + +import ( + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/terminal" + "github.com/cloudfoundry/cli/plugin" + "github.com/cloudfoundry/cli/plugin/models" + "github.com/codegangsta/cli" + + "fmt" + "net" + "net/rpc" + "strconv" +) + +type CliRpcService struct { + listener net.Listener + stopCh chan struct{} + Pinged bool + RpcCmd *CliRpcCmd +} + +type CliRpcCmd struct { + PluginMetadata *plugin.PluginMetadata + coreCommandRunner *cli.App + outputCapture terminal.OutputCapture + terminalOutputSwitch terminal.TerminalOutputSwitch + cliConfig core_config.Repository +} + +func NewRpcService(commandRunner *cli.App, outputCapture terminal.OutputCapture, terminalOutputSwitch terminal.TerminalOutputSwitch, cliConfig core_config.Repository) (*CliRpcService, error) { + rpcService := &CliRpcService{ + RpcCmd: &CliRpcCmd{ + PluginMetadata: &plugin.PluginMetadata{}, + coreCommandRunner: commandRunner, + outputCapture: outputCapture, + terminalOutputSwitch: terminalOutputSwitch, + cliConfig: cliConfig, + }, + } + + err := rpc.Register(rpcService.RpcCmd) + if err != nil { + return nil, err + } + + return rpcService, nil +} + +func (cli *CliRpcService) Stop() { + close(cli.stopCh) + cli.listener.Close() +} + +func (cli *CliRpcService) Port() string { + return strconv.Itoa(cli.listener.Addr().(*net.TCPAddr).Port) +} + +func (cli *CliRpcService) Start() error { + var err error + + cli.stopCh = make(chan struct{}) + + cli.listener, err = net.Listen("tcp", "127.0.0.1:0") + if err != nil { + return err + } + + go func() { + for { + conn, err := cli.listener.Accept() + if err != nil { + select { + case <-cli.stopCh: + return + default: + fmt.Println(err) + } + } else { + go rpc.ServeConn(conn) + } + } + }() + + return nil +} + +func (cmd *CliRpcService) SetTheApp(app *cli.App) { + cmd.RpcCmd.coreCommandRunner = app +} + +func (cmd *CliRpcCmd) SetPluginMetadata(pluginMetadata plugin.PluginMetadata, retVal *bool) error { + cmd.PluginMetadata = &pluginMetadata + *retVal = true + return nil +} + +func (cmd *CliRpcCmd) DisableTerminalOutput(disable bool, retVal *bool) error { + cmd.terminalOutputSwitch.DisableTerminalOutput(disable) + *retVal = true + return nil +} + +func (cmd *CliRpcCmd) CallCoreCommand(args []string, retVal *bool) error { + defer func() { + recover() + }() + + err := cmd.coreCommandRunner.Run(append([]string{"CF_NAME"}, args...)) + + if err != nil { + *retVal = false + return err + } + + *retVal = true + return nil +} + +func (cmd *CliRpcCmd) GetOutputAndReset(args bool, retVal *[]string) error { + *retVal = cmd.outputCapture.GetOutputAndReset() + return nil +} + +func (cmd *CliRpcCmd) GetCurrentOrg(args string, retVal *plugin_models.Organization) error { + retVal.Name = cmd.cliConfig.OrganizationFields().Name + retVal.Guid = cmd.cliConfig.OrganizationFields().Guid + retVal.QuotaDefinition.Guid = cmd.cliConfig.OrganizationFields().QuotaDefinition.Guid + retVal.QuotaDefinition.Name = cmd.cliConfig.OrganizationFields().QuotaDefinition.Name + retVal.QuotaDefinition.MemoryLimit = cmd.cliConfig.OrganizationFields().QuotaDefinition.MemoryLimit + retVal.QuotaDefinition.InstanceMemoryLimit = cmd.cliConfig.OrganizationFields().QuotaDefinition.InstanceMemoryLimit + retVal.QuotaDefinition.RoutesLimit = cmd.cliConfig.OrganizationFields().QuotaDefinition.RoutesLimit + retVal.QuotaDefinition.ServicesLimit = cmd.cliConfig.OrganizationFields().QuotaDefinition.ServicesLimit + retVal.QuotaDefinition.NonBasicServicesAllowed = cmd.cliConfig.OrganizationFields().QuotaDefinition.NonBasicServicesAllowed + + return nil +} + +func (cmd *CliRpcCmd) GetCurrentSpace(args string, retVal *plugin_models.Space) error { + retVal.Name = cmd.cliConfig.SpaceFields().Name + retVal.Guid = cmd.cliConfig.SpaceFields().Guid + + return nil +} + +func (cmd *CliRpcCmd) Username(args string, retVal *string) error { + *retVal = cmd.cliConfig.Username() + + return nil +} + +func (cmd *CliRpcCmd) UserGuid(args string, retVal *string) error { + *retVal = cmd.cliConfig.UserGuid() + + return nil +} + +func (cmd *CliRpcCmd) UserEmail(args string, retVal *string) error { + *retVal = cmd.cliConfig.UserEmail() + + return nil +} + +func (cmd *CliRpcCmd) IsLoggedIn(args string, retVal *bool) error { + *retVal = cmd.cliConfig.IsLoggedIn() + + return nil +} + +func (cmd *CliRpcCmd) IsSSLDisabled(args string, retVal *bool) error { + *retVal = cmd.cliConfig.IsSSLDisabled() + + return nil +} + +func (cmd *CliRpcCmd) HasOrganization(args string, retVal *bool) error { + *retVal = cmd.cliConfig.HasOrganization() + + return nil +} + +func (cmd *CliRpcCmd) HasSpace(args string, retVal *bool) error { + *retVal = cmd.cliConfig.HasSpace() + + return nil +} + +func (cmd *CliRpcCmd) ApiEndpoint(args string, retVal *string) error { + *retVal = cmd.cliConfig.ApiEndpoint() + + return nil +} + +func (cmd *CliRpcCmd) HasAPIEndpoint(args string, retVal *bool) error { + *retVal = cmd.cliConfig.HasAPIEndpoint() + + return nil +} + +func (cmd *CliRpcCmd) ApiVersion(args string, retVal *string) error { + *retVal = cmd.cliConfig.ApiVersion() + + return nil +} + +func (cmd *CliRpcCmd) LoggregatorEndpoint(args string, retVal *string) error { + *retVal = cmd.cliConfig.LoggregatorEndpoint() + + return nil +} + +func (cmd *CliRpcCmd) DopplerEndpoint(args string, retVal *string) error { + *retVal = cmd.cliConfig.DopplerEndpoint() + + return nil +} + +func (cmd *CliRpcCmd) AccessToken(args string, retVal *string) error { + *retVal = cmd.cliConfig.AccessToken() + + return nil +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/plugin/rpc/cli_rpc_server_test.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/plugin/rpc/cli_rpc_server_test.go new file mode 100644 index 0000000..57caf28 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/plugin/rpc/cli_rpc_server_test.go @@ -0,0 +1,584 @@ +package rpc_test + +import ( + "net" + "net/rpc" + "time" + + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/cf/models" + "github.com/cloudfoundry/cli/cf/terminal/fakes" + "github.com/cloudfoundry/cli/plugin" + "github.com/cloudfoundry/cli/plugin/models" + . "github.com/cloudfoundry/cli/plugin/rpc" + testconfig "github.com/cloudfoundry/cli/testhelpers/configuration" + io_helpers "github.com/cloudfoundry/cli/testhelpers/io" + "github.com/codegangsta/cli" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Server", func() { + var ( + err error + client *rpc.Client + rpcService *CliRpcService + ) + + AfterEach(func() { + if client != nil { + client.Close() + } + }) + + BeforeEach(func() { + rpc.DefaultServer = rpc.NewServer() + }) + + Describe(".NewRpcService", func() { + BeforeEach(func() { + rpcService, err = NewRpcService(nil, nil, nil, nil) + Expect(err).ToNot(HaveOccurred()) + }) + + It("returns an err of another Rpc process is already registered", func() { + _, err := NewRpcService(nil, nil, nil, nil) + Expect(err).To(HaveOccurred()) + }) + }) + + Describe(".Stop", func() { + BeforeEach(func() { + rpcService, err = NewRpcService(nil, nil, nil, nil) + Expect(err).ToNot(HaveOccurred()) + + err := rpcService.Start() + Expect(err).ToNot(HaveOccurred()) + + pingCli(rpcService.Port()) + }) + + It("shuts down the rpc server", func() { + rpcService.Stop() + + //give time for server to stop + time.Sleep(50 * time.Millisecond) + + client, err = rpc.Dial("tcp", "127.0.0.1:"+rpcService.Port()) + Expect(err).To(HaveOccurred()) + }) + }) + + Describe(".Start", func() { + BeforeEach(func() { + rpcService, err = NewRpcService(nil, nil, nil, nil) + Expect(err).ToNot(HaveOccurred()) + + err := rpcService.Start() + Expect(err).ToNot(HaveOccurred()) + + pingCli(rpcService.Port()) + }) + + AfterEach(func() { + rpcService.Stop() + + //give time for server to stop + time.Sleep(50 * time.Millisecond) + }) + + It("Start an Rpc server for communication", func() { + client, err = rpc.Dial("tcp", "127.0.0.1:"+rpcService.Port()) + Expect(err).ToNot(HaveOccurred()) + }) + }) + + Describe(".SetPluginMetadata", func() { + var ( + metadata *plugin.PluginMetadata + ) + + BeforeEach(func() { + rpcService, err = NewRpcService(nil, nil, nil, nil) + Expect(err).ToNot(HaveOccurred()) + + err := rpcService.Start() + Expect(err).ToNot(HaveOccurred()) + + pingCli(rpcService.Port()) + + client, err = rpc.Dial("tcp", "127.0.0.1:"+rpcService.Port()) + Expect(err).ToNot(HaveOccurred()) + + metadata = &plugin.PluginMetadata{ + Name: "foo", + Commands: []plugin.Command{ + {Name: "cmd_1", HelpText: "cm 1 help text"}, + {Name: "cmd_2", HelpText: "cmd 2 help text"}, + }, + } + }) + + AfterEach(func() { + rpcService.Stop() + + //give time for server to stop + time.Sleep(50 * time.Millisecond) + }) + + It("set the rpc command's Return Data", func() { + var success bool + err = client.Call("CliRpcCmd.SetPluginMetadata", metadata, &success) + + Expect(err).ToNot(HaveOccurred()) + Expect(success).To(BeTrue()) + Expect(rpcService.RpcCmd.PluginMetadata).To(Equal(metadata)) + }) + }) + + Describe(".GetOutputAndReset", func() { + Context("success", func() { + BeforeEach(func() { + outputCapture := &fakes.FakeOutputCapture{} + outputCapture.GetOutputAndResetReturns([]string{"hi from command"}) + rpcService, err = NewRpcService(nil, outputCapture, nil, nil) + Expect(err).ToNot(HaveOccurred()) + + err := rpcService.Start() + Expect(err).ToNot(HaveOccurred()) + + pingCli(rpcService.Port()) + }) + + AfterEach(func() { + rpcService.Stop() + + //give time for server to stop + time.Sleep(50 * time.Millisecond) + }) + + It("should return the logs from the output capture", func() { + client, err = rpc.Dial("tcp", "127.0.0.1:"+rpcService.Port()) + Expect(err).ToNot(HaveOccurred()) + var output []string + client.Call("CliRpcCmd.GetOutputAndReset", false, &output) + + Expect(output).To(Equal([]string{"hi from command"})) + }) + }) + }) + + Describe("disabling terminal output", func() { + var terminalOutputSwitch *fakes.FakeTerminalOutputSwitch + + BeforeEach(func() { + terminalOutputSwitch = &fakes.FakeTerminalOutputSwitch{} + rpcService, err = NewRpcService(nil, nil, terminalOutputSwitch, nil) + Expect(err).ToNot(HaveOccurred()) + + err := rpcService.Start() + Expect(err).ToNot(HaveOccurred()) + + pingCli(rpcService.Port()) + }) + + It("should disable the terminal output switch", func() { + client, err = rpc.Dial("tcp", "127.0.0.1:"+rpcService.Port()) + Expect(err).ToNot(HaveOccurred()) + + var success bool + err = client.Call("CliRpcCmd.DisableTerminalOutput", true, &success) + + Expect(err).ToNot(HaveOccurred()) + Expect(success).To(BeTrue()) + Expect(terminalOutputSwitch.DisableTerminalOutputCallCount()).To(Equal(1)) + Expect(terminalOutputSwitch.DisableTerminalOutputArgsForCall(0)).To(Equal(true)) + }) + }) + + Describe(".CallCoreCommand", func() { + Context("success", func() { + BeforeEach(func() { + app := &cli.App{ + Commands: []cli.Command{ + { + Name: "test_cmd", + Description: "test_cmd description", + Usage: "test_cmd usage", + Action: func(context *cli.Context) { + return + }, + }, + }, + } + + outputCapture := &fakes.FakeOutputCapture{} + + rpcService, err = NewRpcService(app, outputCapture, nil, nil) + Expect(err).ToNot(HaveOccurred()) + + err := rpcService.Start() + Expect(err).ToNot(HaveOccurred()) + + pingCli(rpcService.Port()) + }) + + AfterEach(func() { + rpcService.Stop() + + //give time for server to stop + time.Sleep(50 * time.Millisecond) + }) + + It("calls the code gangsta cli App command", func() { + client, err = rpc.Dial("tcp", "127.0.0.1:"+rpcService.Port()) + Expect(err).ToNot(HaveOccurred()) + + var success bool + err = client.Call("CliRpcCmd.CallCoreCommand", []string{"test_cmd"}, &success) + + Expect(err).ToNot(HaveOccurred()) + Expect(success).To(BeTrue()) + }) + }) + + Describe("CLI Config object methods", func() { + var ( + config core_config.Repository + ) + + BeforeEach(func() { + config = testconfig.NewRepositoryWithDefaults() + }) + + AfterEach(func() { + rpcService.Stop() + + //give time for server to stop + time.Sleep(50 * time.Millisecond) + }) + + Context(".GetCurrentOrg", func() { + BeforeEach(func() { + config.SetOrganizationFields(models.OrganizationFields{ + Guid: "test-guid", + Name: "test-org", + QuotaDefinition: models.QuotaFields{ + Guid: "guid123", + Name: "quota123", + MemoryLimit: 128, + InstanceMemoryLimit: 16, + RoutesLimit: 5, + ServicesLimit: 6, + NonBasicServicesAllowed: true, + }, + }) + + rpcService, err = NewRpcService(nil, nil, nil, config) + err := rpcService.Start() + Expect(err).ToNot(HaveOccurred()) + + pingCli(rpcService.Port()) + }) + + It("populates the plugin Organization object with the current org settings in config", func() { + client, err = rpc.Dial("tcp", "127.0.0.1:"+rpcService.Port()) + Expect(err).ToNot(HaveOccurred()) + + var org plugin_models.Organization + err = client.Call("CliRpcCmd.GetCurrentOrg", "", &org) + + Expect(err).ToNot(HaveOccurred()) + Expect(org.Name).To(Equal("test-org")) + Expect(org.Guid).To(Equal("test-guid")) + Expect(org.QuotaDefinition.Guid).To(Equal("guid123")) + Expect(org.QuotaDefinition.Name).To(Equal("quota123")) + Expect(org.QuotaDefinition.MemoryLimit).To(Equal(int64(128))) + Expect(org.QuotaDefinition.InstanceMemoryLimit).To(Equal(int64(16))) + Expect(org.QuotaDefinition.RoutesLimit).To(Equal(5)) + Expect(org.QuotaDefinition.ServicesLimit).To(Equal(6)) + Expect(org.QuotaDefinition.NonBasicServicesAllowed).To(BeTrue()) + }) + }) + + Context(".GetCurrentSpace", func() { + BeforeEach(func() { + config.SetSpaceFields(models.SpaceFields{ + Guid: "space-guid", + Name: "space-name", + }) + + rpcService, err = NewRpcService(nil, nil, nil, config) + err := rpcService.Start() + Expect(err).ToNot(HaveOccurred()) + + pingCli(rpcService.Port()) + }) + + It("populates the plugin Space object with the current space settings in config", func() { + client, err = rpc.Dial("tcp", "127.0.0.1:"+rpcService.Port()) + Expect(err).ToNot(HaveOccurred()) + + var space plugin_models.Space + err = client.Call("CliRpcCmd.GetCurrentSpace", "", &space) + + Expect(err).ToNot(HaveOccurred()) + Expect(space.Name).To(Equal("space-name")) + Expect(space.Guid).To(Equal("space-guid")) + }) + }) + + Context(".Username, .UserGuid, .UserEmail", func() { + BeforeEach(func() { + rpcService, err = NewRpcService(nil, nil, nil, config) + err := rpcService.Start() + Expect(err).ToNot(HaveOccurred()) + + pingCli(rpcService.Port()) + }) + + It("returns username, user guid and user email", func() { + client, err = rpc.Dial("tcp", "127.0.0.1:"+rpcService.Port()) + Expect(err).ToNot(HaveOccurred()) + + var result string + err = client.Call("CliRpcCmd.Username", "", &result) + Expect(err).ToNot(HaveOccurred()) + Expect(result).To(Equal("my-user")) + + err = client.Call("CliRpcCmd.UserGuid", "", &result) + Expect(err).ToNot(HaveOccurred()) + Expect(result).To(Equal("my-user-guid")) + + err = client.Call("CliRpcCmd.UserEmail", "", &result) + Expect(err).ToNot(HaveOccurred()) + Expect(result).To(Equal("my-user-email")) + }) + }) + + Context(".IsSSLDisabled", func() { + BeforeEach(func() { + rpcService, err = NewRpcService(nil, nil, nil, config) + err := rpcService.Start() + Expect(err).ToNot(HaveOccurred()) + + pingCli(rpcService.Port()) + }) + + It("returns the IsSSLDisabled setting in config", func() { + config.SetSSLDisabled(true) + client, err = rpc.Dial("tcp", "127.0.0.1:"+rpcService.Port()) + Expect(err).ToNot(HaveOccurred()) + + var result bool + err = client.Call("CliRpcCmd.IsSSLDisabled", "", &result) + Expect(err).ToNot(HaveOccurred()) + Expect(result).To(BeTrue()) + }) + }) + + Context(".IsLoggedIn", func() { + BeforeEach(func() { + rpcService, err = NewRpcService(nil, nil, nil, config) + err := rpcService.Start() + Expect(err).ToNot(HaveOccurred()) + + pingCli(rpcService.Port()) + }) + + It("returns the IsLoggedIn setting in config", func() { + config.SetAccessToken("Logged-In-Token") + client, err = rpc.Dial("tcp", "127.0.0.1:"+rpcService.Port()) + Expect(err).ToNot(HaveOccurred()) + + var result bool + err = client.Call("CliRpcCmd.IsLoggedIn", "", &result) + Expect(err).ToNot(HaveOccurred()) + Expect(result).To(BeTrue()) + }) + }) + + Context(".HasOrganization and .HasSpace ", func() { + BeforeEach(func() { + rpcService, err = NewRpcService(nil, nil, nil, config) + err := rpcService.Start() + Expect(err).ToNot(HaveOccurred()) + + pingCli(rpcService.Port()) + }) + + It("returns the HasOrganization() and HasSpace() setting in config", func() { + client, err = rpc.Dial("tcp", "127.0.0.1:"+rpcService.Port()) + Expect(err).ToNot(HaveOccurred()) + + var result bool + err = client.Call("CliRpcCmd.HasOrganization", "", &result) + Expect(err).ToNot(HaveOccurred()) + Expect(result).To(BeTrue()) + + err = client.Call("CliRpcCmd.HasSpace", "", &result) + Expect(err).ToNot(HaveOccurred()) + Expect(result).To(BeTrue()) + }) + }) + + Context(".LoggregatorEndpoint and .DopplerEndpoint ", func() { + BeforeEach(func() { + rpcService, err = NewRpcService(nil, nil, nil, config) + err := rpcService.Start() + Expect(err).ToNot(HaveOccurred()) + + pingCli(rpcService.Port()) + }) + + It("returns the LoggregatorEndpoint() and DopplerEndpoint() setting in config", func() { + config.SetLoggregatorEndpoint("loggregator-endpoint-sample") + config.SetDopplerEndpoint("doppler-endpoint-sample") + + client, err = rpc.Dial("tcp", "127.0.0.1:"+rpcService.Port()) + Expect(err).ToNot(HaveOccurred()) + + var result string + err = client.Call("CliRpcCmd.LoggregatorEndpoint", "", &result) + Expect(err).ToNot(HaveOccurred()) + Expect(result).To(Equal("loggregator-endpoint-sample")) + + err = client.Call("CliRpcCmd.DopplerEndpoint", "", &result) + Expect(err).ToNot(HaveOccurred()) + Expect(result).To(Equal("doppler-endpoint-sample")) + }) + }) + + Context(".ApiEndpoint, .ApiVersion and .HasAPIEndpoint", func() { + BeforeEach(func() { + rpcService, err = NewRpcService(nil, nil, nil, config) + err := rpcService.Start() + Expect(err).ToNot(HaveOccurred()) + + pingCli(rpcService.Port()) + }) + + It("returns the ApiEndpoint(), ApiVersion() and HasAPIEndpoint() setting in config", func() { + config.SetApiVersion("v1.1.1") + config.SetApiEndpoint("www.fake-domain.com") + + client, err = rpc.Dial("tcp", "127.0.0.1:"+rpcService.Port()) + Expect(err).ToNot(HaveOccurred()) + + var result string + err = client.Call("CliRpcCmd.ApiEndpoint", "", &result) + Expect(err).ToNot(HaveOccurred()) + Expect(result).To(Equal("www.fake-domain.com")) + + err = client.Call("CliRpcCmd.ApiVersion", "", &result) + Expect(err).ToNot(HaveOccurred()) + Expect(result).To(Equal("v1.1.1")) + + var exists bool + err = client.Call("CliRpcCmd.HasAPIEndpoint", "", &exists) + Expect(err).ToNot(HaveOccurred()) + Expect(exists).To(BeTrue()) + + }) + }) + + Context(".AccessToken", func() { + BeforeEach(func() { + rpcService, err = NewRpcService(nil, nil, nil, config) + err := rpcService.Start() + Expect(err).ToNot(HaveOccurred()) + + pingCli(rpcService.Port()) + }) + + It("returns the LoggregatorEndpoint() and DopplerEndpoint() setting in config", func() { + config.SetAccessToken("fake-access-token") + + client, err = rpc.Dial("tcp", "127.0.0.1:"+rpcService.Port()) + Expect(err).ToNot(HaveOccurred()) + + var result string + err = client.Call("CliRpcCmd.AccessToken", "", &result) + Expect(err).ToNot(HaveOccurred()) + Expect(result).To(Equal("fake-access-token")) + }) + }) + + }) + + Context("fail", func() { + BeforeEach(func() { + app := &cli.App{ + Commands: []cli.Command{ + { + Name: "test_cmd", + Description: "test_cmd description", + Usage: "test_cmd usage", + Action: func(context *cli.Context) { + panic("ERROR") + }, + }, + }, + } + outputCapture := &fakes.FakeOutputCapture{} + rpcService, err = NewRpcService(app, outputCapture, nil, nil) + Expect(err).ToNot(HaveOccurred()) + + err := rpcService.Start() + Expect(err).ToNot(HaveOccurred()) + + pingCli(rpcService.Port()) + }) + + It("returns false in success if the command cannot be found", func() { + io_helpers.CaptureOutput(func() { + client, err = rpc.Dial("tcp", "127.0.0.1:"+rpcService.Port()) + Expect(err).ToNot(HaveOccurred()) + + var success bool + err = client.Call("CliRpcCmd.CallCoreCommand", []string{"not_a_cmd"}, &success) + Expect(success).To(BeFalse()) + Expect(err).ToNot(HaveOccurred()) + }) + }) + + It("returns an error if a command cannot parse provided flags", func() { + io_helpers.CaptureOutput(func() { + client, err = rpc.Dial("tcp", "127.0.0.1:"+rpcService.Port()) + Expect(err).ToNot(HaveOccurred()) + + var success bool + err = client.Call("CliRpcCmd.CallCoreCommand", []string{"test_cmd", "-invalid_flag"}, &success) + + Expect(err).To(HaveOccurred()) + Expect(success).To(BeFalse()) + }) + }) + + It("recovers from a panic from any core command", func() { + client, err = rpc.Dial("tcp", "127.0.0.1:"+rpcService.Port()) + Expect(err).ToNot(HaveOccurred()) + + var success bool + err = client.Call("CliRpcCmd.CallCoreCommand", []string{"test_cmd"}, &success) + + Expect(success).To(BeFalse()) + }) + }) + }) +}) + +func pingCli(port string) { + var connErr error + var conn net.Conn + for i := 0; i < 5; i++ { + conn, connErr = net.Dial("tcp", "127.0.0.1:"+port) + if connErr != nil { + time.Sleep(200 * time.Millisecond) + } else { + conn.Close() + break + } + } + Expect(connErr).ToNot(HaveOccurred()) +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/plugin/rpc/rpc_suite_test.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/plugin/rpc/rpc_suite_test.go new file mode 100644 index 0000000..094a948 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/plugin/rpc/rpc_suite_test.go @@ -0,0 +1,16 @@ +package rpc_test + +import ( + "github.com/cloudfoundry/cli/plugin/rpc" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +var rpcService *rpc.CliRpcService + +func TestRpc(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Rpc Suite") +} diff --git a/Godeps/_workspace/src/github.com/cloudfoundry/cli/plugin/rpc/run_plugin.go b/Godeps/_workspace/src/github.com/cloudfoundry/cli/plugin/rpc/run_plugin.go new file mode 100644 index 0000000..9d89784 --- /dev/null +++ b/Godeps/_workspace/src/github.com/cloudfoundry/cli/plugin/rpc/run_plugin.go @@ -0,0 +1,40 @@ +package rpc + +import ( + "os" + "os/exec" + + "github.com/cloudfoundry/cli/cf/configuration/plugin_config" +) + +func RunMethodIfExists(rpcService *CliRpcService, args []string, pluginList map[string]plugin_config.PluginMetadata) bool { + for _, metadata := range pluginList { + for _, command := range metadata.Commands { + if command.Name == args[0] || command.Alias == args[0] { + args[0] = command.Name + + rpcService.Start() + defer rpcService.Stop() + + pluginArgs := append([]string{rpcService.Port()}, args...) + + cmd := exec.Command(metadata.Location, pluginArgs...) + cmd.Stdout = os.Stdout + cmd.Stdin = os.Stdin + + defer stopPlugin(cmd) + err := cmd.Run() + if err != nil { + os.Exit(1) + } + return true + } + } + } + return false +} + +func stopPlugin(plugin *exec.Cmd) { + plugin.Process.Kill() + plugin.Wait() +} diff --git a/Godeps/_workspace/src/github.com/gizak/termui/.gitignore b/Godeps/_workspace/src/github.com/gizak/termui/.gitignore new file mode 100644 index 0000000..daf913b --- /dev/null +++ b/Godeps/_workspace/src/github.com/gizak/termui/.gitignore @@ -0,0 +1,24 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof diff --git a/Godeps/_workspace/src/github.com/gizak/termui/.travis.yml b/Godeps/_workspace/src/github.com/gizak/termui/.travis.yml new file mode 100644 index 0000000..206e887 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gizak/termui/.travis.yml @@ -0,0 +1,6 @@ +language: go + +go: + - tip + +script: go test -v ./ \ No newline at end of file diff --git a/Godeps/_workspace/src/github.com/gizak/termui/LICENSE b/Godeps/_workspace/src/github.com/gizak/termui/LICENSE new file mode 100644 index 0000000..311ccc7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gizak/termui/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2015 Zack Guo + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/Godeps/_workspace/src/github.com/gizak/termui/README.md b/Godeps/_workspace/src/github.com/gizak/termui/README.md new file mode 100644 index 0000000..01458a4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gizak/termui/README.md @@ -0,0 +1,156 @@ +# termui [![Build Status](https://travis-ci.org/gizak/termui.svg?branch=master)](https://travis-ci.org/gizak/termui) [![Doc Status](https://godoc.org/github.com/gizak/termui?status.png)](https://godoc.org/github.com/gizak/termui) + +## Notice +termui comes with ABSOLUTELY NO WARRANTY, and there is a breaking change coming up (see refactoring branch) which will change the `Bufferer` interface and many others. These changes reduce calculation overhead and introduce a new drawing buffer with better capacibilities. We will step into the next stage (call it beta) after merging these changes. + +## Introduction +Go terminal dashboard. Inspired by [blessed-contrib](https://github.com/yaronn/blessed-contrib), but purely in Go. + +Cross-platform, easy to compile, and fully-customizable. + +__Demo:__ (cast under osx 10.10; Terminal.app; Menlo Regular 12pt.) + +demo + +__Grid layout:__ + +Expressive syntax, using [12 columns grid system](http://www.w3schools.com/bootstrap/bootstrap_grid_system.asp) +```go + import ui "github.com/gizak/termui" + // init and create widgets... + + // build + ui.Body.AddRows( + ui.NewRow( + ui.NewCol(6, 0, widget0), + ui.NewCol(6, 0, widget1)), + ui.NewRow( + ui.NewCol(3, 0, widget2), + ui.NewCol(3, 0, widget30, widget31, widget32), + ui.NewCol(6, 0, widget4))) + + // calculate layout + ui.Body.Align() + + ui.Render(ui.Body) +``` +[demo code:](https://github.com/gizak/termui/blob/master/example/grid.go) + +grid + +## Installation + + go get github.com/gizak/termui + +## Usage + +Each component's layout is a bit like HTML block (box model), which has border and padding. + +The `Border` property can be chosen to hide or display (with its border label), when it comes to display, the label takes 1 padding space (i.e. in css: `padding: 1;`, innerHeight and innerWidth therefore shrunk by 1). + +`````go + import ui "github.com/gizak/termui" // <- ui shortcut, optional + + func main() { + err := ui.Init() + if err != nil { + panic(err) + } + defer ui.Close() + + p := ui.NewPar(":PRESS q TO QUIT DEMO") + p.Height = 3 + p.Width = 50 + p.TextFgColor = ui.ColorWhite + p.Border.Label = "Text Box" + p.Border.FgColor = ui.ColorCyan + + g := ui.NewGauge() + g.Percent = 50 + g.Width = 50 + g.Height = 3 + g.Y = 11 + g.Border.Label = "Gauge" + g.BarColor = ui.ColorRed + g.Border.FgColor = ui.ColorWhite + g.Border.LabelFgColor = ui.ColorCyan + + ui.Render(p, g) + + // event handler... + } +````` + +Note that components can be overlapped (I'd rather call this a feature...), `Render(rs ...Renderer)` renders its args from left to right (i.e. each component's weight is arising from left to right). + +## Themes + +_All_ colors in _all_ components can be changed at _any_ time, while there provides some predefined color schemes: + +```go +// for now there are only two themes: default and helloworld +termui.UseTheme("helloworld") + +// create components... +``` +The `default ` theme's settings depend on the user's terminal color scheme, which is saying if your terminal default font color is white and background is white, it will be like: + +default + +The `helloworld` color scheme drops in some colors! + +helloworld + +## Widgets + +#### Par + +[demo code](https://github.com/gizak/termui/blob/master/example/par.go) + +par + +#### List +[demo code](https://github.com/gizak/termui/blob/master/example/list.go) + +list + +#### Gauge +[demo code](https://github.com/gizak/termui/blob/master/example/gauge.go) + +gauge + +#### Line Chart +[demo code](https://github.com/gizak/termui/blob/master/example/linechart.go) + +linechart + +#### Bar Chart +[demo code](https://github.com/gizak/termui/blob/master/example/barchart.go) + +barchart + +#### Mult-Bar / Stacked-Bar Chart +[demo code](https://github.com/gizak/termui/blob/master/example/mbarchart.go) + +barchart + +#### Sparklines +[demo code](https://github.com/gizak/termui/blob/master/example/sparklines.go) + +sparklines + + +## GoDoc + +[godoc](https://godoc.org/github.com/gizak/termui) + +## TODO + +- [x] Grid layout +- [ ] Event system +- [ ] Canvas widget +- [ ] Refine APIs +- [ ] Focusable widgets + +## License +This library is under the [MIT License](http://opensource.org/licenses/MIT) diff --git a/Godeps/_workspace/src/github.com/gizak/termui/bar.go b/Godeps/_workspace/src/github.com/gizak/termui/bar.go new file mode 100644 index 0000000..57bae0a --- /dev/null +++ b/Godeps/_workspace/src/github.com/gizak/termui/bar.go @@ -0,0 +1,135 @@ +// Copyright 2015 Zack Guo . All rights reserved. +// Use of this source code is governed by a MIT license that can +// be found in the LICENSE file. + +package termui + +import "fmt" + +// BarChart creates multiple bars in a widget: +/* + bc := termui.NewBarChart() + data := []int{3, 2, 5, 3, 9, 5} + bclabels := []string{"S0", "S1", "S2", "S3", "S4", "S5"} + bc.Border.Label = "Bar Chart" + bc.Data = data + bc.Width = 26 + bc.Height = 10 + bc.DataLabels = bclabels + bc.TextColor = termui.ColorGreen + bc.BarColor = termui.ColorRed + bc.NumColor = termui.ColorYellow +*/ +type BarChart struct { + Block + BarColor Attribute + TextColor Attribute + NumColor Attribute + Data []int + DataLabels []string + BarWidth int + BarGap int + labels [][]rune + dataNum [][]rune + numBar int + scale float64 + max int +} + +// NewBarChart returns a new *BarChart with current theme. +func NewBarChart() *BarChart { + bc := &BarChart{Block: *NewBlock()} + bc.BarColor = theme.BarChartBar + bc.NumColor = theme.BarChartNum + bc.TextColor = theme.BarChartText + bc.BarGap = 1 + bc.BarWidth = 3 + return bc +} + +func (bc *BarChart) layout() { + bc.numBar = bc.innerWidth / (bc.BarGap + bc.BarWidth) + bc.labels = make([][]rune, bc.numBar) + bc.dataNum = make([][]rune, len(bc.Data)) + + for i := 0; i < bc.numBar && i < len(bc.DataLabels) && i < len(bc.Data); i++ { + bc.labels[i] = trimStr2Runes(bc.DataLabels[i], bc.BarWidth) + n := bc.Data[i] + s := fmt.Sprint(n) + bc.dataNum[i] = trimStr2Runes(s, bc.BarWidth) + } + + //bc.max = bc.Data[0] // what if Data is nil? Sometimes when bar graph is nill it produces panic with panic: runtime error: index out of range + // Asign a negative value to get maxvalue auto-populates + if bc.max == 0 { + bc.max = -1 + } + for i := 0; i < len(bc.Data); i++ { + if bc.max < bc.Data[i] { + bc.max = bc.Data[i] + } + } + bc.scale = float64(bc.max) / float64(bc.innerHeight-1) +} + +func (bc *BarChart) SetMax(max int) { + + if max > 0 { + bc.max = max + } +} + +// Buffer implements Bufferer interface. +func (bc *BarChart) Buffer() []Point { + ps := bc.Block.Buffer() + bc.layout() + + for i := 0; i < bc.numBar && i < len(bc.Data) && i < len(bc.DataLabels); i++ { + h := int(float64(bc.Data[i]) / bc.scale) + oftX := i * (bc.BarWidth + bc.BarGap) + // plot bar + for j := 0; j < bc.BarWidth; j++ { + for k := 0; k < h; k++ { + p := Point{} + p.Ch = ' ' + p.Bg = bc.BarColor + if bc.BarColor == ColorDefault { // when color is default, space char treated as transparent! + p.Bg |= AttrReverse + } + p.X = bc.innerX + i*(bc.BarWidth+bc.BarGap) + j + p.Y = bc.innerY + bc.innerHeight - 2 - k + ps = append(ps, p) + } + } + // plot text + for j, k := 0, 0; j < len(bc.labels[i]); j++ { + w := charWidth(bc.labels[i][j]) + p := Point{} + p.Ch = bc.labels[i][j] + p.Bg = bc.BgColor + p.Fg = bc.TextColor + p.Y = bc.innerY + bc.innerHeight - 1 + p.X = bc.innerX + oftX + k + ps = append(ps, p) + k += w + } + // plot num + for j := 0; j < len(bc.dataNum[i]); j++ { + p := Point{} + p.Ch = bc.dataNum[i][j] + p.Fg = bc.NumColor + p.Bg = bc.BarColor + if bc.BarColor == ColorDefault { // the same as above + p.Bg |= AttrReverse + } + if h == 0 { + p.Bg = bc.BgColor + } + p.X = bc.innerX + oftX + (bc.BarWidth-len(bc.dataNum[i]))/2 + j + p.Y = bc.innerY + bc.innerHeight - 2 + ps = append(ps, p) + } + } + + return bc.Block.chopOverflow(ps) +} diff --git a/Godeps/_workspace/src/github.com/gizak/termui/block.go b/Godeps/_workspace/src/github.com/gizak/termui/block.go new file mode 100644 index 0000000..9531365 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gizak/termui/block.go @@ -0,0 +1,142 @@ +// Copyright 2015 Zack Guo . All rights reserved. +// Use of this source code is governed by a MIT license that can +// be found in the LICENSE file. + +package termui + +// Block is a base struct for all other upper level widgets, +// consider it as css: display:block. +// Normally you do not need to create it manually. +type Block struct { + X int + Y int + Border labeledBorder + IsDisplay bool + HasBorder bool + BgColor Attribute + Width int + Height int + innerWidth int + innerHeight int + innerX int + innerY int + PaddingTop int + PaddingBottom int + PaddingLeft int + PaddingRight int +} + +// NewBlock returns a *Block which inherits styles from current theme. +func NewBlock() *Block { + d := Block{} + d.IsDisplay = true + d.HasBorder = theme.HasBorder + d.Border.BgColor = theme.BorderBg + d.Border.FgColor = theme.BorderFg + d.Border.LabelBgColor = theme.BorderLabelTextBg + d.Border.LabelFgColor = theme.BorderLabelTextFg + d.BgColor = theme.BlockBg + d.Width = 2 + d.Height = 2 + return &d +} + +// compute box model +func (d *Block) align() { + d.innerWidth = d.Width - d.PaddingLeft - d.PaddingRight + d.innerHeight = d.Height - d.PaddingTop - d.PaddingBottom + d.innerX = d.X + d.PaddingLeft + d.innerY = d.Y + d.PaddingTop + + if d.HasBorder { + d.innerHeight -= 2 + d.innerWidth -= 2 + d.Border.X = d.X + d.Border.Y = d.Y + d.Border.Width = d.Width + d.Border.Height = d.Height + d.innerX++ + d.innerY++ + } + + if d.innerHeight < 0 { + d.innerHeight = 0 + } + if d.innerWidth < 0 { + d.innerWidth = 0 + } + +} + +// InnerBounds returns the internal bounds of the block after aligning and +// calculating the padding and border, if any. +func (d *Block) InnerBounds() (x, y, width, height int) { + d.align() + return d.innerX, d.innerY, d.innerWidth, d.innerHeight +} + +// Buffer implements Bufferer interface. +// Draw background and border (if any). +func (d *Block) Buffer() []Point { + d.align() + + ps := []Point{} + if !d.IsDisplay { + return ps + } + + if d.HasBorder { + ps = d.Border.Buffer() + } + + for i := 0; i < d.innerWidth; i++ { + for j := 0; j < d.innerHeight; j++ { + p := Point{} + p.X = d.X + 1 + i + p.Y = d.Y + 1 + j + p.Ch = ' ' + p.Bg = d.BgColor + ps = append(ps, p) + } + } + return ps +} + +// GetHeight implements GridBufferer. +// It returns current height of the block. +func (d Block) GetHeight() int { + return d.Height +} + +// SetX implements GridBufferer interface, which sets block's x position. +func (d *Block) SetX(x int) { + d.X = x +} + +// SetY implements GridBufferer interface, it sets y position for block. +func (d *Block) SetY(y int) { + d.Y = y +} + +// SetWidth implements GridBuffer interface, it sets block's width. +func (d *Block) SetWidth(w int) { + d.Width = w +} + +// chop the overflow parts +func (d *Block) chopOverflow(ps []Point) []Point { + nps := make([]Point, 0, len(ps)) + x := d.X + y := d.Y + w := d.Width + h := d.Height + for _, v := range ps { + if v.X >= x && + v.X < x+w && + v.Y >= y && + v.Y < y+h { + nps = append(nps, v) + } + } + return nps +} diff --git a/Godeps/_workspace/src/github.com/gizak/termui/block_test.go b/Godeps/_workspace/src/github.com/gizak/termui/block_test.go new file mode 100644 index 0000000..2de205b --- /dev/null +++ b/Godeps/_workspace/src/github.com/gizak/termui/block_test.go @@ -0,0 +1,46 @@ +package termui + +import "testing" + +func TestBlock_InnerBounds(t *testing.T) { + b := NewBlock() + b.X = 10 + b.Y = 11 + b.Width = 12 + b.Height = 13 + + assert := func(name string, x, y, w, h int) { + t.Log(name) + cx, cy, cw, ch := b.InnerBounds() + if cx != x { + t.Errorf("expected x to be %d but got %d", x, cx) + } + if cy != y { + t.Errorf("expected y to be %d but got %d", y, cy) + } + if cw != w { + t.Errorf("expected width to be %d but got %d", w, cw) + } + if ch != h { + t.Errorf("expected height to be %d but got %d", h, ch) + } + } + + b.HasBorder = false + assert("no border, no padding", 10, 11, 12, 13) + + b.HasBorder = true + assert("border, no padding", 11, 12, 10, 11) + + b.PaddingBottom = 2 + assert("border, 2b padding", 11, 12, 10, 9) + + b.PaddingTop = 3 + assert("border, 2b 3t padding", 11, 15, 10, 6) + + b.PaddingLeft = 4 + assert("border, 2b 3t 4l padding", 15, 15, 6, 6) + + b.PaddingRight = 5 + assert("border, 2b 3t 4l 5r padding", 15, 15, 1, 6) +} diff --git a/Godeps/_workspace/src/github.com/gizak/termui/box.go b/Godeps/_workspace/src/github.com/gizak/termui/box.go new file mode 100644 index 0000000..1dcfd86 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gizak/termui/box.go @@ -0,0 +1,117 @@ +// Copyright 2015 Zack Guo . All rights reserved. +// Use of this source code is governed by a MIT license that can +// be found in the LICENSE file. + +package termui + +type border struct { + X int + Y int + Width int + Height int + FgColor Attribute + BgColor Attribute +} + +type hline struct { + X int + Y int + Length int + FgColor Attribute + BgColor Attribute +} + +type vline struct { + X int + Y int + Length int + FgColor Attribute + BgColor Attribute +} + +// Draw a horizontal line. +func (l hline) Buffer() []Point { + pts := make([]Point, l.Length) + for i := 0; i < l.Length; i++ { + pts[i].X = l.X + i + pts[i].Y = l.Y + pts[i].Ch = HORIZONTAL_LINE + pts[i].Bg = l.BgColor + pts[i].Fg = l.FgColor + } + return pts +} + +// Draw a vertical line. +func (l vline) Buffer() []Point { + pts := make([]Point, l.Length) + for i := 0; i < l.Length; i++ { + pts[i].X = l.X + pts[i].Y = l.Y + i + pts[i].Ch = VERTICAL_LINE + pts[i].Bg = l.BgColor + pts[i].Fg = l.FgColor + } + return pts +} + +// Draw a box border. +func (b border) Buffer() []Point { + if b.Width < 2 || b.Height < 2 { + return nil + } + pts := make([]Point, 2*b.Width+2*b.Height-4) + + pts[0].X = b.X + pts[0].Y = b.Y + pts[0].Fg = b.FgColor + pts[0].Bg = b.BgColor + pts[0].Ch = TOP_LEFT + + pts[1].X = b.X + b.Width - 1 + pts[1].Y = b.Y + pts[1].Fg = b.FgColor + pts[1].Bg = b.BgColor + pts[1].Ch = TOP_RIGHT + + pts[2].X = b.X + pts[2].Y = b.Y + b.Height - 1 + pts[2].Fg = b.FgColor + pts[2].Bg = b.BgColor + pts[2].Ch = BOTTOM_LEFT + + pts[3].X = b.X + b.Width - 1 + pts[3].Y = b.Y + b.Height - 1 + pts[3].Fg = b.FgColor + pts[3].Bg = b.BgColor + pts[3].Ch = BOTTOM_RIGHT + + copy(pts[4:], (hline{b.X + 1, b.Y, b.Width - 2, b.FgColor, b.BgColor}).Buffer()) + copy(pts[4+b.Width-2:], (hline{b.X + 1, b.Y + b.Height - 1, b.Width - 2, b.FgColor, b.BgColor}).Buffer()) + copy(pts[4+2*b.Width-4:], (vline{b.X, b.Y + 1, b.Height - 2, b.FgColor, b.BgColor}).Buffer()) + copy(pts[4+2*b.Width-4+b.Height-2:], (vline{b.X + b.Width - 1, b.Y + 1, b.Height - 2, b.FgColor, b.BgColor}).Buffer()) + + return pts +} + +type labeledBorder struct { + border + Label string + LabelFgColor Attribute + LabelBgColor Attribute +} + +// Draw a box border with label. +func (lb labeledBorder) Buffer() []Point { + ps := lb.border.Buffer() + maxTxtW := lb.Width - 2 + rs := trimStr2Runes(lb.Label, maxTxtW) + + for i, j, w := 0, 0, 0; i < len(rs); i++ { + w = charWidth(rs[i]) + ps = append(ps, newPointWithAttrs(rs[i], lb.X+1+j, lb.Y, lb.LabelFgColor, lb.LabelBgColor)) + j += w + } + + return ps +} diff --git a/Godeps/_workspace/src/github.com/gizak/termui/box_others.go b/Godeps/_workspace/src/github.com/gizak/termui/box_others.go new file mode 100644 index 0000000..bcc3d7d --- /dev/null +++ b/Godeps/_workspace/src/github.com/gizak/termui/box_others.go @@ -0,0 +1,14 @@ +// Copyright 2015 Zack Guo . All rights reserved. +// Use of this source code is governed by a MIT license that can +// be found in the LICENSE file. + +// +build !windows + +package termui + +const TOP_RIGHT = '┐' +const VERTICAL_LINE = '│' +const HORIZONTAL_LINE = '─' +const TOP_LEFT = '┌' +const BOTTOM_RIGHT = '┘' +const BOTTOM_LEFT = '└' diff --git a/Godeps/_workspace/src/github.com/gizak/termui/box_windows.go b/Godeps/_workspace/src/github.com/gizak/termui/box_windows.go new file mode 100644 index 0000000..dd39019 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gizak/termui/box_windows.go @@ -0,0 +1,14 @@ +// Copyright 2015 Zack Guo . All rights reserved. +// Use of this source code is governed by a MIT license that can +// be found in the LICENSE file. + +// +build windows + +package termui + +const TOP_RIGHT = '+' +const VERTICAL_LINE = '|' +const HORIZONTAL_LINE = '-' +const TOP_LEFT = '+' +const BOTTOM_RIGHT = '+' +const BOTTOM_LEFT = '+' diff --git a/Godeps/_workspace/src/github.com/gizak/termui/canvas.go b/Godeps/_workspace/src/github.com/gizak/termui/canvas.go new file mode 100644 index 0000000..614635e --- /dev/null +++ b/Godeps/_workspace/src/github.com/gizak/termui/canvas.go @@ -0,0 +1,74 @@ +// Copyright 2015 Zack Guo . All rights reserved. +// Use of this source code is governed by a MIT license that can +// be found in the LICENSE file. + +package termui + +/* +dots: + ,___, + |1 4| + |2 5| + |3 6| + |7 8| + ````` +*/ + +var brailleBase = '\u2800' + +var brailleOftMap = [4][2]rune{ + {'\u0001', '\u0008'}, + {'\u0002', '\u0010'}, + {'\u0004', '\u0020'}, + {'\u0040', '\u0080'}} + +// Canvas contains drawing map: i,j -> rune +type Canvas map[[2]int]rune + +// NewCanvas returns an empty Canvas +func NewCanvas() Canvas { + return make(map[[2]int]rune) +} + +func chOft(x, y int) rune { + return brailleOftMap[y%4][x%2] +} + +func (c Canvas) rawCh(x, y int) rune { + if ch, ok := c[[2]int{x, y}]; ok { + return ch + } + return '\u0000' //brailleOffset +} + +// return coordinate in terminal +func chPos(x, y int) (int, int) { + return y / 4, x / 2 +} + +// Set sets a point (x,y) in the virtual coordinate +func (c Canvas) Set(x, y int) { + i, j := chPos(x, y) + ch := c.rawCh(i, j) + ch |= chOft(x, y) + c[[2]int{i, j}] = ch +} + +// Unset removes point (x,y) +func (c Canvas) Unset(x, y int) { + i, j := chPos(x, y) + ch := c.rawCh(i, j) + ch &= ^chOft(x, y) + c[[2]int{i, j}] = ch +} + +// Buffer returns un-styled points +func (c Canvas) Buffer() []Point { + ps := make([]Point, len(c)) + i := 0 + for k, v := range c { + ps[i] = newPoint(v+brailleBase, k[0], k[1]) + i++ + } + return ps +} diff --git a/Godeps/_workspace/src/github.com/gizak/termui/canvas_test.go b/Godeps/_workspace/src/github.com/gizak/termui/canvas_test.go new file mode 100644 index 0000000..021949c --- /dev/null +++ b/Godeps/_workspace/src/github.com/gizak/termui/canvas_test.go @@ -0,0 +1,55 @@ +package termui + +import ( + "testing" + + "github.com/davecgh/go-spew/spew" +) + +func TestCanvasSet(t *testing.T) { + c := NewCanvas() + c.Set(0, 0) + c.Set(0, 1) + c.Set(0, 2) + c.Set(0, 3) + c.Set(1, 3) + c.Set(2, 3) + c.Set(3, 3) + c.Set(4, 3) + c.Set(5, 3) + spew.Dump(c) +} + +func TestCanvasUnset(t *testing.T) { + c := NewCanvas() + c.Set(0, 0) + c.Set(0, 1) + c.Set(0, 2) + c.Unset(0, 2) + spew.Dump(c) + c.Unset(0, 3) + spew.Dump(c) +} + +func TestCanvasBuffer(t *testing.T) { + c := NewCanvas() + c.Set(0, 0) + c.Set(0, 1) + c.Set(0, 2) + c.Set(0, 3) + c.Set(1, 3) + c.Set(2, 3) + c.Set(3, 3) + c.Set(4, 3) + c.Set(5, 3) + c.Set(6, 3) + c.Set(7, 2) + c.Set(8, 1) + c.Set(9, 0) + bufs := c.Buffer() + rs := make([]rune, len(bufs)) + for i, v := range bufs { + rs[i] = v.Ch + } + spew.Dump(string(rs)) +} diff --git a/Godeps/_workspace/src/github.com/gizak/termui/chart.go b/Godeps/_workspace/src/github.com/gizak/termui/chart.go new file mode 100644 index 0000000..d6fb8bc --- /dev/null +++ b/Godeps/_workspace/src/github.com/gizak/termui/chart.go @@ -0,0 +1,336 @@ +// Copyright 2015 Zack Guo . All rights reserved. +// Use of this source code is governed by a MIT license that can +// be found in the LICENSE file. + +package termui + +import ( + "fmt" + "math" +) + +// only 16 possible combinations, why bother +var braillePatterns = map[[2]int]rune{ + [2]int{0, 0}: '⣀', + [2]int{0, 1}: '⡠', + [2]int{0, 2}: '⡐', + [2]int{0, 3}: '⡈', + + [2]int{1, 0}: '⢄', + [2]int{1, 1}: '⠤', + [2]int{1, 2}: '⠔', + [2]int{1, 3}: '⠌', + + [2]int{2, 0}: '⢂', + [2]int{2, 1}: '⠢', + [2]int{2, 2}: '⠒', + [2]int{2, 3}: '⠊', + + [2]int{3, 0}: '⢁', + [2]int{3, 1}: '⠡', + [2]int{3, 2}: '⠑', + [2]int{3, 3}: '⠉', +} + +var lSingleBraille = [4]rune{'\u2840', '⠄', '⠂', '⠁'} +var rSingleBraille = [4]rune{'\u2880', '⠠', '⠐', '⠈'} + +// LineChart has two modes: braille(default) and dot. Using braille gives 2x capicity as dot mode, +// because one braille char can represent two data points. +/* + lc := termui.NewLineChart() + lc.Border.Label = "braille-mode Line Chart" + lc.Data = [1.2, 1.3, 1.5, 1.7, 1.5, 1.6, 1.8, 2.0] + lc.Width = 50 + lc.Height = 12 + lc.AxesColor = termui.ColorWhite + lc.LineColor = termui.ColorGreen | termui.AttrBold + // termui.Render(lc)... +*/ +type LineChart struct { + Block + Data []float64 + DataLabels []string // if unset, the data indices will be used + Mode string // braille | dot + DotStyle rune + LineColor Attribute + scale float64 // data span per cell on y-axis + AxesColor Attribute + drawingX int + drawingY int + axisYHeight int + axisXWidth int + axisYLebelGap int + axisXLebelGap int + topValue float64 + bottomValue float64 + labelX [][]rune + labelY [][]rune + labelYSpace int + maxY float64 + minY float64 +} + +// NewLineChart returns a new LineChart with current theme. +func NewLineChart() *LineChart { + lc := &LineChart{Block: *NewBlock()} + lc.AxesColor = theme.LineChartAxes + lc.LineColor = theme.LineChartLine + lc.Mode = "braille" + lc.DotStyle = '•' + lc.axisXLebelGap = 2 + lc.axisYLebelGap = 1 + lc.bottomValue = math.Inf(1) + lc.topValue = math.Inf(-1) + return lc +} + +// one cell contains two data points +// so the capicity is 2x as dot-mode +func (lc *LineChart) renderBraille() []Point { + ps := []Point{} + + // return: b -> which cell should the point be in + // m -> in the cell, divided into 4 equal height levels, which subcell? + getPos := func(d float64) (b, m int) { + cnt4 := int((d-lc.bottomValue)/(lc.scale/4) + 0.5) + b = cnt4 / 4 + m = cnt4 % 4 + return + } + // plot points + for i := 0; 2*i+1 < len(lc.Data) && i < lc.axisXWidth; i++ { + b0, m0 := getPos(lc.Data[2*i]) + b1, m1 := getPos(lc.Data[2*i+1]) + + if b0 == b1 { + p := Point{} + p.Ch = braillePatterns[[2]int{m0, m1}] + p.Bg = lc.BgColor + p.Fg = lc.LineColor + p.Y = lc.innerY + lc.innerHeight - 3 - b0 + p.X = lc.innerX + lc.labelYSpace + 1 + i + ps = append(ps, p) + } else { + p0 := newPointWithAttrs(lSingleBraille[m0], + lc.innerX+lc.labelYSpace+1+i, + lc.innerY+lc.innerHeight-3-b0, + lc.LineColor, + lc.BgColor) + p1 := newPointWithAttrs(rSingleBraille[m1], + lc.innerX+lc.labelYSpace+1+i, + lc.innerY+lc.innerHeight-3-b1, + lc.LineColor, + lc.BgColor) + ps = append(ps, p0, p1) + } + + } + return ps +} + +func (lc *LineChart) renderDot() []Point { + ps := []Point{} + for i := 0; i < len(lc.Data) && i < lc.axisXWidth; i++ { + p := Point{} + p.Ch = lc.DotStyle + p.Fg = lc.LineColor + p.Bg = lc.BgColor + p.X = lc.innerX + lc.labelYSpace + 1 + i + p.Y = lc.innerY + lc.innerHeight - 3 - int((lc.Data[i]-lc.bottomValue)/lc.scale+0.5) + ps = append(ps, p) + } + + return ps +} + +func (lc *LineChart) calcLabelX() { + lc.labelX = [][]rune{} + + for i, l := 0, 0; i < len(lc.DataLabels) && l < lc.axisXWidth; i++ { + if lc.Mode == "dot" { + if l >= len(lc.DataLabels) { + break + } + + s := str2runes(lc.DataLabels[l]) + w := strWidth(lc.DataLabels[l]) + if l+w <= lc.axisXWidth { + lc.labelX = append(lc.labelX, s) + } + l += w + lc.axisXLebelGap + } else { // braille + if 2*l >= len(lc.DataLabels) { + break + } + + s := str2runes(lc.DataLabels[2*l]) + w := strWidth(lc.DataLabels[2*l]) + if l+w <= lc.axisXWidth { + lc.labelX = append(lc.labelX, s) + } + l += w + lc.axisXLebelGap + + } + } +} + +func shortenFloatVal(x float64) string { + s := fmt.Sprintf("%.2f", x) + if len(s)-3 > 3 { + s = fmt.Sprintf("%.2e", x) + } + + if x < 0 { + s = fmt.Sprintf("%.2f", x) + } + return s +} + +func (lc *LineChart) calcLabelY() { + span := lc.topValue - lc.bottomValue + lc.scale = span / float64(lc.axisYHeight) + + n := (1 + lc.axisYHeight) / (lc.axisYLebelGap + 1) + lc.labelY = make([][]rune, n) + maxLen := 0 + for i := 0; i < n; i++ { + s := str2runes(shortenFloatVal(lc.bottomValue + float64(i)*span/float64(n))) + if len(s) > maxLen { + maxLen = len(s) + } + lc.labelY[i] = s + } + + lc.labelYSpace = maxLen +} + +func (lc *LineChart) calcLayout() { + // set datalabels if it is not provided + if lc.DataLabels == nil || len(lc.DataLabels) == 0 { + lc.DataLabels = make([]string, len(lc.Data)) + for i := range lc.Data { + lc.DataLabels[i] = fmt.Sprint(i) + } + } + + // lazy increase, to avoid y shaking frequently + // update bound Y when drawing is gonna overflow + lc.minY = lc.Data[0] + lc.maxY = lc.Data[0] + + // valid visible range + vrange := lc.innerWidth + if lc.Mode == "braille" { + vrange = 2 * lc.innerWidth + } + if vrange > len(lc.Data) { + vrange = len(lc.Data) + } + + for _, v := range lc.Data[:vrange] { + if v > lc.maxY { + lc.maxY = v + } + if v < lc.minY { + lc.minY = v + } + } + + span := lc.maxY - lc.minY + + if lc.minY < lc.bottomValue { + lc.bottomValue = lc.minY - 0.2*span + } + + if lc.maxY > lc.topValue { + lc.topValue = lc.maxY + 0.2*span + } + + lc.axisYHeight = lc.innerHeight - 2 + lc.calcLabelY() + + lc.axisXWidth = lc.innerWidth - 1 - lc.labelYSpace + lc.calcLabelX() + + lc.drawingX = lc.innerX + 1 + lc.labelYSpace + lc.drawingY = lc.innerY +} + +func (lc *LineChart) plotAxes() []Point { + origY := lc.innerY + lc.innerHeight - 2 + origX := lc.innerX + lc.labelYSpace + + ps := []Point{newPointWithAttrs(ORIGIN, origX, origY, lc.AxesColor, lc.BgColor)} + + for x := origX + 1; x < origX+lc.axisXWidth; x++ { + p := Point{} + p.X = x + p.Y = origY + p.Bg = lc.BgColor + p.Fg = lc.AxesColor + p.Ch = HDASH + ps = append(ps, p) + } + + for dy := 1; dy <= lc.axisYHeight; dy++ { + p := Point{} + p.X = origX + p.Y = origY - dy + p.Bg = lc.BgColor + p.Fg = lc.AxesColor + p.Ch = VDASH + ps = append(ps, p) + } + + // x label + oft := 0 + for _, rs := range lc.labelX { + if oft+len(rs) > lc.axisXWidth { + break + } + for j, r := range rs { + p := Point{} + p.Ch = r + p.Fg = lc.AxesColor + p.Bg = lc.BgColor + p.X = origX + oft + j + p.Y = lc.innerY + lc.innerHeight - 1 + ps = append(ps, p) + } + oft += len(rs) + lc.axisXLebelGap + } + + // y labels + for i, rs := range lc.labelY { + for j, r := range rs { + p := Point{} + p.Ch = r + p.Fg = lc.AxesColor + p.Bg = lc.BgColor + p.X = lc.innerX + j + p.Y = origY - i*(lc.axisYLebelGap+1) + ps = append(ps, p) + } + } + + return ps +} + +// Buffer implements Bufferer interface. +func (lc *LineChart) Buffer() []Point { + ps := lc.Block.Buffer() + if lc.Data == nil || len(lc.Data) == 0 { + return ps + } + lc.calcLayout() + ps = append(ps, lc.plotAxes()...) + + if lc.Mode == "dot" { + ps = append(ps, lc.renderDot()...) + } else { + ps = append(ps, lc.renderBraille()...) + } + + return lc.Block.chopOverflow(ps) +} diff --git a/Godeps/_workspace/src/github.com/gizak/termui/chart_others.go b/Godeps/_workspace/src/github.com/gizak/termui/chart_others.go new file mode 100644 index 0000000..8911873 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gizak/termui/chart_others.go @@ -0,0 +1,11 @@ +// Copyright 2015 Zack Guo . All rights reserved. +// Use of this source code is governed by a MIT license that can +// be found in the LICENSE file. + +// +build !windows + +package termui + +const VDASH = '┊' +const HDASH = '┈' +const ORIGIN = '└' diff --git a/Godeps/_workspace/src/github.com/gizak/termui/chart_windows.go b/Godeps/_workspace/src/github.com/gizak/termui/chart_windows.go new file mode 100644 index 0000000..9f9a5e9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gizak/termui/chart_windows.go @@ -0,0 +1,11 @@ +// Copyright 2015 Zack Guo . All rights reserved. +// Use of this source code is governed by a MIT license that can +// be found in the LICENSE file. + +// +build windows + +package termui + +const VDASH = '|' +const HDASH = '-' +const ORIGIN = '+' diff --git a/Godeps/_workspace/src/github.com/gizak/termui/doc.go b/Godeps/_workspace/src/github.com/gizak/termui/doc.go new file mode 100644 index 0000000..43f886f --- /dev/null +++ b/Godeps/_workspace/src/github.com/gizak/termui/doc.go @@ -0,0 +1,27 @@ +// Copyright 2015 Zack Guo . All rights reserved. +// Use of this source code is governed by a MIT license that can +// be found in the LICENSE file. + +/* +Package termui is a library designed for creating command line UI. For more info, goto http://github.com/gizak/termui + +A simplest example: + package main + + import ui "github.com/gizak/termui" + + func main() { + if err:=ui.Init(); err != nil { + panic(err) + } + defer ui.Close() + + g := ui.NewGauge() + g.Percent = 50 + g.Width = 50 + g.Border.Label = "Gauge" + + ui.Render(g) + } +*/ +package termui diff --git a/Godeps/_workspace/src/github.com/gizak/termui/events.go b/Godeps/_workspace/src/github.com/gizak/termui/events.go new file mode 100644 index 0000000..23a189b --- /dev/null +++ b/Godeps/_workspace/src/github.com/gizak/termui/events.go @@ -0,0 +1,219 @@ +// Copyright 2015 Zack Guo . All rights reserved. +// Use of this source code is governed by a MIT license that can +// be found in the LICENSE file. +// +// Portions of this file uses [termbox-go](https://github.com/nsf/termbox-go/blob/54b74d087b7c397c402d0e3b66d2ccb6eaf5c2b4/api_common.go) +// by [authors](https://github.com/nsf/termbox-go/blob/master/AUTHORS) +// under [license](https://github.com/nsf/termbox-go/blob/master/LICENSE) + +package termui + +import "github.com/nsf/termbox-go" + +/***********************************termbox-go**************************************/ + +type ( + EventType uint8 + Modifier uint8 + Key uint16 +) + +// This type represents a termbox event. The 'Mod', 'Key' and 'Ch' fields are +// valid if 'Type' is EventKey. The 'Width' and 'Height' fields are valid if +// 'Type' is EventResize. The 'Err' field is valid if 'Type' is EventError. +type Event struct { + Type EventType // one of Event* constants + Mod Modifier // one of Mod* constants or 0 + Key Key // one of Key* constants, invalid if 'Ch' is not 0 + Ch rune // a unicode character + Width int // width of the screen + Height int // height of the screen + Err error // error in case if input failed + MouseX int // x coord of mouse + MouseY int // y coord of mouse + N int // number of bytes written when getting a raw event +} + +const ( + KeyF1 Key = 0xFFFF - iota + KeyF2 + KeyF3 + KeyF4 + KeyF5 + KeyF6 + KeyF7 + KeyF8 + KeyF9 + KeyF10 + KeyF11 + KeyF12 + KeyInsert + KeyDelete + KeyHome + KeyEnd + KeyPgup + KeyPgdn + KeyArrowUp + KeyArrowDown + KeyArrowLeft + KeyArrowRight + key_min // see terminfo + MouseLeft + MouseMiddle + MouseRight +) + +const ( + KeyCtrlTilde Key = 0x00 + KeyCtrl2 Key = 0x00 + KeyCtrlSpace Key = 0x00 + KeyCtrlA Key = 0x01 + KeyCtrlB Key = 0x02 + KeyCtrlC Key = 0x03 + KeyCtrlD Key = 0x04 + KeyCtrlE Key = 0x05 + KeyCtrlF Key = 0x06 + KeyCtrlG Key = 0x07 + KeyBackspace Key = 0x08 + KeyCtrlH Key = 0x08 + KeyTab Key = 0x09 + KeyCtrlI Key = 0x09 + KeyCtrlJ Key = 0x0A + KeyCtrlK Key = 0x0B + KeyCtrlL Key = 0x0C + KeyEnter Key = 0x0D + KeyCtrlM Key = 0x0D + KeyCtrlN Key = 0x0E + KeyCtrlO Key = 0x0F + KeyCtrlP Key = 0x10 + KeyCtrlQ Key = 0x11 + KeyCtrlR Key = 0x12 + KeyCtrlS Key = 0x13 + KeyCtrlT Key = 0x14 + KeyCtrlU Key = 0x15 + KeyCtrlV Key = 0x16 + KeyCtrlW Key = 0x17 + KeyCtrlX Key = 0x18 + KeyCtrlY Key = 0x19 + KeyCtrlZ Key = 0x1A + KeyEsc Key = 0x1B + KeyCtrlLsqBracket Key = 0x1B + KeyCtrl3 Key = 0x1B + KeyCtrl4 Key = 0x1C + KeyCtrlBackslash Key = 0x1C + KeyCtrl5 Key = 0x1D + KeyCtrlRsqBracket Key = 0x1D + KeyCtrl6 Key = 0x1E + KeyCtrl7 Key = 0x1F + KeyCtrlSlash Key = 0x1F + KeyCtrlUnderscore Key = 0x1F + KeySpace Key = 0x20 + KeyBackspace2 Key = 0x7F + KeyCtrl8 Key = 0x7F +) + +// Alt modifier constant, see Event.Mod field and SetInputMode function. +const ( + ModAlt Modifier = 0x01 +) + +// Event type. See Event.Type field. +const ( + EventKey EventType = iota + EventResize + EventMouse + EventError + EventInterrupt + EventRaw + EventNone +) + +/**************************************end**************************************/ + +// convert termbox.Event to termui.Event +func uiEvt(e termbox.Event) Event { + event := Event{} + event.Type = EventType(e.Type) + event.Mod = Modifier(e.Mod) + event.Key = Key(e.Key) + event.Ch = e.Ch + event.Width = e.Width + event.Height = e.Height + event.Err = e.Err + event.MouseX = e.MouseX + event.MouseY = e.MouseY + event.N = e.N + + return event +} + +var evtChs = make([]chan Event, 0) + +// EventCh returns an output-only event channel. +// This function can be called many times (multiplexer). +func EventCh() <-chan Event { + out := make(chan Event) + evtChs = append(evtChs, out) + return out +} + +// turn on event listener +func evtListen() { + go func() { + for { + e := termbox.PollEvent() + // dispatch + for _, c := range evtChs { + go func(ch chan Event) { + ch <- uiEvt(e) + }(c) + } + } + }() +} + +/* +// EventHandlers is a handler sequence +var EventHandlers []func(Event) + +var signalQuit = make(chan bool) + +// Quit sends quit signal to terminate termui +func Quit() { + signalQuit <- true +} + +// Wait listening to signalQuit, block operation. +func Wait() { + <-signalQuit +} + +// RegEvtHandler register function into TSEventHandler sequence. +func RegEvtHandler(fn func(Event)) { + EventHandlers = append(EventHandlers, fn) +} + +// EventLoop handles all events and +// redirects every event to callbacks in EventHandlers +func EventLoop() { + evt := make(chan termbox.Event) + + go func() { + for { + evt <- termbox.PollEvent() + } + }() + + for { + select { + case c := <-signalQuit: + defer func() { signalQuit <- c }() + return + case e := <-evt: + for _, fn := range EventHandlers { + fn(uiEvt(e)) + } + } + } +} +*/ diff --git a/Godeps/_workspace/src/github.com/gizak/termui/events_test.go b/Godeps/_workspace/src/github.com/gizak/termui/events_test.go new file mode 100644 index 0000000..1137b1d --- /dev/null +++ b/Godeps/_workspace/src/github.com/gizak/termui/events_test.go @@ -0,0 +1,28 @@ +// Copyright 2015 Zack Guo . All rights reserved. +// Use of this source code is governed by a MIT license that can +// be found in the LICENSE file. +// +// Portions of this file uses [termbox-go](https://github.com/nsf/termbox-go/blob/54b74d087b7c397c402d0e3b66d2ccb6eaf5c2b4/api_common.go) +// by [authors](https://github.com/nsf/termbox-go/blob/master/AUTHORS) +// under [license](https://github.com/nsf/termbox-go/blob/master/LICENSE) + +package termui + +import ( + "errors" + "testing" + + termbox "github.com/nsf/termbox-go" + "github.com/stretchr/testify/assert" +) + +type boxEvent termbox.Event + +func TestUiEvt(t *testing.T) { + err := errors.New("This is a mock error") + event := boxEvent{3, 5, 2, 'H', 200, 500, err, 50, 30, 2} + expetced := Event{3, 5, 2, 'H', 200, 500, err, 50, 30, 2} + + // We need to do that ugly casting so that vet does not complain + assert.Equal(t, uiEvt(termbox.Event(event)), expetced) +} diff --git a/Godeps/_workspace/src/github.com/gizak/termui/example/barchart.go b/Godeps/_workspace/src/github.com/gizak/termui/example/barchart.go new file mode 100644 index 0000000..83947f5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gizak/termui/example/barchart.go @@ -0,0 +1,35 @@ +// Copyright 2015 Zack Guo . All rights reserved. +// Use of this source code is governed by a MIT license that can +// be found in the LICENSE file. + +// +build ignore + +package main + +import "github.com/gizak/termui" + +func main() { + err := termui.Init() + if err != nil { + panic(err) + } + defer termui.Close() + + termui.UseTheme("helloworld") + + bc := termui.NewBarChart() + data := []int{3, 2, 5, 3, 9, 5, 3, 2, 5, 8, 3, 2, 4, 5, 3, 2, 5, 7, 5, 3, 2, 6, 7, 4, 6, 3, 6, 7, 8, 3, 6, 4, 5, 3, 2, 4, 6, 4, 8, 5, 9, 4, 3, 6, 5, 3, 6} + bclabels := []string{"S0", "S1", "S2", "S3", "S4", "S5"} + bc.Border.Label = "Bar Chart" + bc.Data = data + bc.Width = 26 + bc.Height = 10 + bc.DataLabels = bclabels + bc.TextColor = termui.ColorGreen + bc.BarColor = termui.ColorRed + bc.NumColor = termui.ColorYellow + + termui.Render(bc) + + <-termui.EventCh() +} diff --git a/Godeps/_workspace/src/github.com/gizak/termui/example/barchart.png b/Godeps/_workspace/src/github.com/gizak/termui/example/barchart.png new file mode 100644 index 0000000..a37912f Binary files /dev/null and b/Godeps/_workspace/src/github.com/gizak/termui/example/barchart.png differ diff --git a/Godeps/_workspace/src/github.com/gizak/termui/example/dashboard.gif b/Godeps/_workspace/src/github.com/gizak/termui/example/dashboard.gif new file mode 100644 index 0000000..8e1859c Binary files /dev/null and b/Godeps/_workspace/src/github.com/gizak/termui/example/dashboard.gif differ diff --git a/Godeps/_workspace/src/github.com/gizak/termui/example/dashboard.go b/Godeps/_workspace/src/github.com/gizak/termui/example/dashboard.go new file mode 100644 index 0000000..c14bb44 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gizak/termui/example/dashboard.go @@ -0,0 +1,148 @@ +// Copyright 2015 Zack Guo . All rights reserved. +// Use of this source code is governed by a MIT license that can +// be found in the LICENSE file. + +// +build ignore + +package main + +import ui "github.com/gizak/termui" +import "math" + +import "time" + +func main() { + err := ui.Init() + if err != nil { + panic(err) + } + defer ui.Close() + + p := ui.NewPar(":PRESS q TO QUIT DEMO") + p.Height = 3 + p.Width = 50 + p.TextFgColor = ui.ColorWhite + p.Border.Label = "Text Box" + p.Border.FgColor = ui.ColorCyan + + strs := []string{"[0] gizak/termui", "[1] editbox.go", "[2] iterrupt.go", "[3] keyboard.go", "[4] output.go", "[5] random_out.go", "[6] dashboard.go", "[7] nsf/termbox-go"} + list := ui.NewList() + list.Items = strs + list.ItemFgColor = ui.ColorYellow + list.Border.Label = "List" + list.Height = 7 + list.Width = 25 + list.Y = 4 + + g := ui.NewGauge() + g.Percent = 50 + g.Width = 50 + g.Height = 3 + g.Y = 11 + g.Border.Label = "Gauge" + g.BarColor = ui.ColorRed + g.Border.FgColor = ui.ColorWhite + g.Border.LabelFgColor = ui.ColorCyan + + spark := ui.Sparkline{} + spark.Height = 1 + spark.Title = "srv 0:" + spdata := []int{4, 2, 1, 6, 3, 9, 1, 4, 2, 15, 14, 9, 8, 6, 10, 13, 15, 12, 10, 5, 3, 6, 1, 7, 10, 10, 14, 13, 6, 4, 2, 1, 6, 3, 9, 1, 4, 2, 15, 14, 9, 8, 6, 10, 13, 15, 12, 10, 5, 3, 6, 1, 7, 10, 10, 14, 13, 6, 4, 2, 1, 6, 3, 9, 1, 4, 2, 15, 14, 9, 8, 6, 10, 13, 15, 12, 10, 5, 3, 6, 1, 7, 10, 10, 14, 13, 6, 4, 2, 1, 6, 3, 9, 1, 4, 2, 15, 14, 9, 8, 6, 10, 13, 15, 12, 10, 5, 3, 6, 1, 7, 10, 10, 14, 13, 6} + spark.Data = spdata + spark.LineColor = ui.ColorCyan + spark.TitleColor = ui.ColorWhite + + spark1 := ui.Sparkline{} + spark1.Height = 1 + spark1.Title = "srv 1:" + spark1.Data = spdata + spark1.TitleColor = ui.ColorWhite + spark1.LineColor = ui.ColorRed + + sp := ui.NewSparklines(spark, spark1) + sp.Width = 25 + sp.Height = 7 + sp.Border.Label = "Sparkline" + sp.Y = 4 + sp.X = 25 + + sinps := (func() []float64 { + n := 220 + ps := make([]float64, n) + for i := range ps { + ps[i] = 1 + math.Sin(float64(i)/5) + } + return ps + })() + + lc := ui.NewLineChart() + lc.Border.Label = "dot-mode Line Chart" + lc.Data = sinps + lc.Width = 50 + lc.Height = 11 + lc.X = 0 + lc.Y = 14 + lc.AxesColor = ui.ColorWhite + lc.LineColor = ui.ColorRed | ui.AttrBold + lc.Mode = "dot" + + bc := ui.NewBarChart() + bcdata := []int{3, 2, 5, 3, 9, 5, 3, 2, 5, 8, 3, 2, 4, 5, 3, 2, 5, 7, 5, 3, 2, 6, 7, 4, 6, 3, 6, 7, 8, 3, 6, 4, 5, 3, 2, 4, 6, 4, 8, 5, 9, 4, 3, 6, 5, 3, 6} + bclabels := []string{"S0", "S1", "S2", "S3", "S4", "S5"} + bc.Border.Label = "Bar Chart" + bc.Width = 26 + bc.Height = 10 + bc.X = 51 + bc.Y = 0 + bc.DataLabels = bclabels + bc.BarColor = ui.ColorGreen + bc.NumColor = ui.ColorBlack + + lc1 := ui.NewLineChart() + lc1.Border.Label = "braille-mode Line Chart" + lc1.Data = sinps + lc1.Width = 26 + lc1.Height = 11 + lc1.X = 51 + lc1.Y = 14 + lc1.AxesColor = ui.ColorWhite + lc1.LineColor = ui.ColorYellow | ui.AttrBold + + p1 := ui.NewPar("Hey!\nI am a borderless block!") + p1.HasBorder = false + p1.Width = 26 + p1.Height = 2 + p1.TextFgColor = ui.ColorMagenta + p1.X = 52 + p1.Y = 11 + + draw := func(t int) { + g.Percent = t % 101 + list.Items = strs[t%9:] + sp.Lines[0].Data = spdata[:30+t%50] + sp.Lines[1].Data = spdata[:35+t%50] + lc.Data = sinps[t/2:] + lc1.Data = sinps[2*t:] + bc.Data = bcdata[t/2%10:] + ui.Render(p, list, g, sp, lc, bc, lc1, p1) + } + + evt := ui.EventCh() + + i := 0 + for { + select { + case e := <-evt: + if e.Type == ui.EventKey && e.Ch == 'q' { + return + } + default: + draw(i) + i++ + if i == 102 { + return + } + time.Sleep(time.Second / 2) + } + } +} diff --git a/Godeps/_workspace/src/github.com/gizak/termui/example/gauge.go b/Godeps/_workspace/src/github.com/gizak/termui/example/gauge.go new file mode 100644 index 0000000..b703358 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gizak/termui/example/gauge.go @@ -0,0 +1,62 @@ +// Copyright 2015 Zack Guo . All rights reserved. +// Use of this source code is governed by a MIT license that can +// be found in the LICENSE file. + +// +build ignore + +package main + +import "github.com/gizak/termui" + +func main() { + err := termui.Init() + if err != nil { + panic(err) + } + defer termui.Close() + + termui.UseTheme("helloworld") + + g0 := termui.NewGauge() + g0.Percent = 40 + g0.Width = 50 + g0.Height = 3 + g0.Border.Label = "Slim Gauge" + g0.BarColor = termui.ColorRed + g0.Border.FgColor = termui.ColorWhite + g0.Border.LabelFgColor = termui.ColorCyan + + g2 := termui.NewGauge() + g2.Percent = 60 + g2.Width = 50 + g2.Height = 3 + g2.PercentColor = termui.ColorBlue + g2.Y = 3 + g2.Border.Label = "Slim Gauge" + g2.BarColor = termui.ColorYellow + g2.Border.FgColor = termui.ColorWhite + + g1 := termui.NewGauge() + g1.Percent = 30 + g1.Width = 50 + g1.Height = 5 + g1.Y = 6 + g1.Border.Label = "Big Gauge" + g1.PercentColor = termui.ColorYellow + g1.BarColor = termui.ColorGreen + g1.Border.FgColor = termui.ColorWhite + g1.Border.LabelFgColor = termui.ColorMagenta + + g3 := termui.NewGauge() + g3.Percent = 50 + g3.Width = 50 + g3.Height = 3 + g3.Y = 11 + g3.Border.Label = "Gauge with custom label" + g3.Label = "{{percent}}% (100MBs free)" + g3.LabelAlign = termui.AlignRight + + termui.Render(g0, g1, g2, g3) + + <-termui.EventCh() +} diff --git a/Godeps/_workspace/src/github.com/gizak/termui/example/gauge.png b/Godeps/_workspace/src/github.com/gizak/termui/example/gauge.png new file mode 100644 index 0000000..5c20e6e Binary files /dev/null and b/Godeps/_workspace/src/github.com/gizak/termui/example/gauge.png differ diff --git a/Godeps/_workspace/src/github.com/gizak/termui/example/grid.gif b/Godeps/_workspace/src/github.com/gizak/termui/example/grid.gif new file mode 100644 index 0000000..7490043 Binary files /dev/null and b/Godeps/_workspace/src/github.com/gizak/termui/example/grid.gif differ diff --git a/Godeps/_workspace/src/github.com/gizak/termui/example/grid.go b/Godeps/_workspace/src/github.com/gizak/termui/example/grid.go new file mode 100644 index 0000000..4912141 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gizak/termui/example/grid.go @@ -0,0 +1,134 @@ +// Copyright 2015 Zack Guo . All rights reserved. +// Use of this source code is governed by a MIT license that can +// be found in the LICENSE file. + +// +build ignore + +package main + +import ui "github.com/gizak/termui" +import "math" +import "time" + +func main() { + err := ui.Init() + if err != nil { + panic(err) + } + defer ui.Close() + + sinps := (func() []float64 { + n := 400 + ps := make([]float64, n) + for i := range ps { + ps[i] = 1 + math.Sin(float64(i)/5) + } + return ps + })() + sinpsint := (func() []int { + ps := make([]int, len(sinps)) + for i, v := range sinps { + ps[i] = int(100*v + 10) + } + return ps + })() + + ui.UseTheme("helloworld") + + spark := ui.Sparkline{} + spark.Height = 8 + spdata := sinpsint + spark.Data = spdata[:100] + spark.LineColor = ui.ColorCyan + spark.TitleColor = ui.ColorWhite + + sp := ui.NewSparklines(spark) + sp.Height = 11 + sp.Border.Label = "Sparkline" + + lc := ui.NewLineChart() + lc.Border.Label = "braille-mode Line Chart" + lc.Data = sinps + lc.Height = 11 + lc.AxesColor = ui.ColorWhite + lc.LineColor = ui.ColorYellow | ui.AttrBold + + gs := make([]*ui.Gauge, 3) + for i := range gs { + gs[i] = ui.NewGauge() + gs[i].Height = 2 + gs[i].HasBorder = false + gs[i].Percent = i * 10 + gs[i].PaddingBottom = 1 + gs[i].BarColor = ui.ColorRed + } + + ls := ui.NewList() + ls.HasBorder = false + ls.Items = []string{ + "[1] Downloading File 1", + "", // == \newline + "[2] Downloading File 2", + "", + "[3] Uploading File 3", + } + ls.Height = 5 + + par := ui.NewPar("<> This row has 3 columns\n<- Widgets can be stacked up like left side\n<- Stacked widgets are treated as a single widget") + par.Height = 5 + par.Border.Label = "Demonstration" + + // build layout + ui.Body.AddRows( + ui.NewRow( + ui.NewCol(6, 0, sp), + ui.NewCol(6, 0, lc)), + ui.NewRow( + ui.NewCol(3, 0, ls), + ui.NewCol(3, 0, gs[0], gs[1], gs[2]), + ui.NewCol(6, 0, par))) + + // calculate layout + ui.Body.Align() + + done := make(chan bool) + redraw := make(chan bool) + + update := func() { + for i := 0; i < 103; i++ { + for _, g := range gs { + g.Percent = (g.Percent + 3) % 100 + } + + sp.Lines[0].Data = spdata[:100+i] + lc.Data = sinps[2*i:] + + time.Sleep(time.Second / 2) + redraw <- true + } + done <- true + } + + evt := ui.EventCh() + + ui.Render(ui.Body) + go update() + + for { + select { + case e := <-evt: + if e.Type == ui.EventKey && e.Ch == 'q' { + return + } + if e.Type == ui.EventResize { + ui.Body.Width = ui.TermWidth() + ui.Body.Align() + go func() { redraw <- true }() + } + case <-done: + return + case <-redraw: + ui.Render(ui.Body) + } + } +} diff --git a/Godeps/_workspace/src/github.com/gizak/termui/example/linechart.go b/Godeps/_workspace/src/github.com/gizak/termui/example/linechart.go new file mode 100644 index 0000000..1db5434 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gizak/termui/example/linechart.go @@ -0,0 +1,68 @@ +// Copyright 2015 Zack Guo . All rights reserved. +// Use of this source code is governed by a MIT license that can +// be found in the LICENSE file. + +// +build ignore + +package main + +import ( + "math" + + "github.com/gizak/termui" +) + +func main() { + err := termui.Init() + if err != nil { + panic(err) + } + defer termui.Close() + + termui.UseTheme("helloworld") + + sinps := (func() []float64 { + n := 220 + ps := make([]float64, n) + for i := range ps { + ps[i] = 1 + math.Sin(float64(i)/5) + } + return ps + })() + + lc0 := termui.NewLineChart() + lc0.Border.Label = "braille-mode Line Chart" + lc0.Data = sinps + lc0.Width = 50 + lc0.Height = 12 + lc0.X = 0 + lc0.Y = 0 + lc0.AxesColor = termui.ColorWhite + lc0.LineColor = termui.ColorGreen | termui.AttrBold + + lc1 := termui.NewLineChart() + lc1.Border.Label = "dot-mode Line Chart" + lc1.Mode = "dot" + lc1.Data = sinps + lc1.Width = 26 + lc1.Height = 12 + lc1.X = 51 + lc1.DotStyle = '+' + lc1.AxesColor = termui.ColorWhite + lc1.LineColor = termui.ColorYellow | termui.AttrBold + + lc2 := termui.NewLineChart() + lc2.Border.Label = "dot-mode Line Chart" + lc2.Mode = "dot" + lc2.Data = sinps[4:] + lc2.Width = 77 + lc2.Height = 16 + lc2.X = 0 + lc2.Y = 12 + lc2.AxesColor = termui.ColorWhite + lc2.LineColor = termui.ColorCyan | termui.AttrBold + + termui.Render(lc0, lc1, lc2) + + <-termui.EventCh() +} diff --git a/Godeps/_workspace/src/github.com/gizak/termui/example/linechart.png b/Godeps/_workspace/src/github.com/gizak/termui/example/linechart.png new file mode 100644 index 0000000..655ef43 Binary files /dev/null and b/Godeps/_workspace/src/github.com/gizak/termui/example/linechart.png differ diff --git a/Godeps/_workspace/src/github.com/gizak/termui/example/list.go b/Godeps/_workspace/src/github.com/gizak/termui/example/list.go new file mode 100644 index 0000000..d33a361 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gizak/termui/example/list.go @@ -0,0 +1,41 @@ +// Copyright 2015 Zack Guo . All rights reserved. +// Use of this source code is governed by a MIT license that can +// be found in the LICENSE file. + +// +build ignore + +package main + +import "github.com/gizak/termui" + +func main() { + err := termui.Init() + if err != nil { + panic(err) + } + defer termui.Close() + + termui.UseTheme("helloworld") + + strs := []string{ + "[0] github.com/gizak/termui", + "[1] 你好,世界", + "[2] こんにちは世界", + "[3] keyboard.go", + "[4] output.go", + "[5] random_out.go", + "[6] dashboard.go", + "[7] nsf/termbox-go"} + + ls := termui.NewList() + ls.Items = strs + ls.ItemFgColor = termui.ColorYellow + ls.Border.Label = "List" + ls.Height = 7 + ls.Width = 25 + ls.Y = 0 + + termui.Render(ls) + + <-termui.EventCh() +} diff --git a/Godeps/_workspace/src/github.com/gizak/termui/example/list.png b/Godeps/_workspace/src/github.com/gizak/termui/example/list.png new file mode 100644 index 0000000..8ca08c0 Binary files /dev/null and b/Godeps/_workspace/src/github.com/gizak/termui/example/list.png differ diff --git a/Godeps/_workspace/src/github.com/gizak/termui/example/mbarchart.go b/Godeps/_workspace/src/github.com/gizak/termui/example/mbarchart.go new file mode 100644 index 0000000..a32a28e --- /dev/null +++ b/Godeps/_workspace/src/github.com/gizak/termui/example/mbarchart.go @@ -0,0 +1,50 @@ +// Copyright 2015 Zack Guo . All rights reserved. +// Use of this source code is governed by a MIT license that can +// be found in the LICENSE file. + +// +build ignore + +package main + +import "github.com/gizak/termui" + +func main() { + err := termui.Init() + if err != nil { + panic(err) + } + defer termui.Close() + + termui.UseTheme("helloworld") + + bc := termui.NewMBarChart() + math := []int{90, 85, 90, 80} + english := []int{70, 85, 75, 60} + science := []int{75, 60, 80, 85} + compsci := []int{100, 100, 100, 100} + bc.Data[0] = math + bc.Data[1] = english + bc.Data[2] = science + bc.Data[3] = compsci + studentsName := []string{"Ken", "Rob", "Dennis", "Linus"} + bc.Border.Label = "Student's Marks X-Axis=Name Y-Axis=Marks[Math,English,Science,ComputerScience] in %" + bc.Width = 100 + bc.Height = 50 + bc.Y = 10 + bc.BarWidth = 10 + bc.DataLabels = studentsName + bc.ShowScale = true //Show y_axis scale value (min and max) + bc.SetMax(400) + + bc.TextColor = termui.ColorGreen //this is color for label (x-axis) + bc.BarColor[3] = termui.ColorGreen //BarColor for computerscience + bc.BarColor[1] = termui.ColorYellow //Bar Color for english + bc.NumColor[3] = termui.ColorRed // Num color for computerscience + bc.NumColor[1] = termui.ColorRed // num color for english + + //Other colors are automatically populated, btw All the students seems do well in computerscience. :p + + termui.Render(bc) + + <-termui.EventCh() +} diff --git a/Godeps/_workspace/src/github.com/gizak/termui/example/mbarchart.png b/Godeps/_workspace/src/github.com/gizak/termui/example/mbarchart.png new file mode 100644 index 0000000..9a42526 Binary files /dev/null and b/Godeps/_workspace/src/github.com/gizak/termui/example/mbarchart.png differ diff --git a/Godeps/_workspace/src/github.com/gizak/termui/example/par.go b/Godeps/_workspace/src/github.com/gizak/termui/example/par.go new file mode 100644 index 0000000..ffbc60a --- /dev/null +++ b/Godeps/_workspace/src/github.com/gizak/termui/example/par.go @@ -0,0 +1,48 @@ +// Copyright 2015 Zack Guo . All rights reserved. +// Use of this source code is governed by a MIT license that can +// be found in the LICENSE file. + +// +build ignore + +package main + +import "github.com/gizak/termui" + +func main() { + err := termui.Init() + if err != nil { + panic(err) + } + defer termui.Close() + + termui.UseTheme("helloworld") + + par0 := termui.NewPar("Borderless Text") + par0.Height = 1 + par0.Width = 20 + par0.Y = 1 + par0.HasBorder = false + + par1 := termui.NewPar("你好,世界。") + par1.Height = 3 + par1.Width = 17 + par1.X = 20 + par1.Border.Label = "标签" + + par2 := termui.NewPar("Simple text\nwith label. It can be multilined with \\n or break automatically") + par2.Height = 5 + par2.Width = 37 + par2.Y = 4 + par2.Border.Label = "Multiline" + par2.Border.FgColor = termui.ColorYellow + + par3 := termui.NewPar("Long text with label and it is auto trimmed.") + par3.Height = 3 + par3.Width = 37 + par3.Y = 9 + par3.Border.Label = "Auto Trim" + + termui.Render(par0, par1, par2, par3) + + <-termui.EventCh() +} diff --git a/Godeps/_workspace/src/github.com/gizak/termui/example/par.png b/Godeps/_workspace/src/github.com/gizak/termui/example/par.png new file mode 100644 index 0000000..a85e644 Binary files /dev/null and b/Godeps/_workspace/src/github.com/gizak/termui/example/par.png differ diff --git a/Godeps/_workspace/src/github.com/gizak/termui/example/sparklines.go b/Godeps/_workspace/src/github.com/gizak/termui/example/sparklines.go new file mode 100644 index 0000000..f04baf5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gizak/termui/example/sparklines.go @@ -0,0 +1,65 @@ +// Copyright 2015 Zack Guo . All rights reserved. +// Use of this source code is governed by a MIT license that can +// be found in the LICENSE file. + +// +build ignore + +package main + +import "github.com/gizak/termui" + +func main() { + err := termui.Init() + if err != nil { + panic(err) + } + defer termui.Close() + + termui.UseTheme("helloworld") + + data := []int{4, 2, 1, 6, 3, 9, 1, 4, 2, 15, 14, 9, 8, 6, 10, 13, 15, 12, 10, 5, 3, 6, 1, 7, 10, 10, 14, 13, 6} + spl0 := termui.NewSparkline() + spl0.Data = data[3:] + spl0.Title = "Sparkline 0" + spl0.LineColor = termui.ColorGreen + + // single + spls0 := termui.NewSparklines(spl0) + spls0.Height = 2 + spls0.Width = 20 + spls0.HasBorder = false + + spl1 := termui.NewSparkline() + spl1.Data = data + spl1.Title = "Sparkline 1" + spl1.LineColor = termui.ColorRed + + spl2 := termui.NewSparkline() + spl2.Data = data[5:] + spl2.Title = "Sparkline 2" + spl2.LineColor = termui.ColorMagenta + + // group + spls1 := termui.NewSparklines(spl0, spl1, spl2) + spls1.Height = 8 + spls1.Width = 20 + spls1.Y = 3 + spls1.Border.Label = "Group Sparklines" + + spl3 := termui.NewSparkline() + spl3.Data = data + spl3.Title = "Enlarged Sparkline" + spl3.Height = 8 + spl3.LineColor = termui.ColorYellow + + spls2 := termui.NewSparklines(spl3) + spls2.Height = 11 + spls2.Width = 30 + spls2.Border.FgColor = termui.ColorCyan + spls2.X = 21 + spls2.Border.Label = "Tweeked Sparkline" + + termui.Render(spls0, spls1, spls2) + + <-termui.EventCh() +} diff --git a/Godeps/_workspace/src/github.com/gizak/termui/example/sparklines.png b/Godeps/_workspace/src/github.com/gizak/termui/example/sparklines.png new file mode 100644 index 0000000..9dd7c82 Binary files /dev/null and b/Godeps/_workspace/src/github.com/gizak/termui/example/sparklines.png differ diff --git a/Godeps/_workspace/src/github.com/gizak/termui/example/theme.go b/Godeps/_workspace/src/github.com/gizak/termui/example/theme.go new file mode 100644 index 0000000..30c51a3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gizak/termui/example/theme.go @@ -0,0 +1,143 @@ +// Copyright 2015 Zack Guo . All rights reserved. +// Use of this source code is governed by a MIT license that can +// be found in the LICENSE file. + +// +build ignore + +package main + +import ui "github.com/gizak/termui" +import "math" + +import "time" + +func main() { + err := ui.Init() + if err != nil { + panic(err) + } + defer ui.Close() + + ui.UseTheme("helloworld") + + p := ui.NewPar(":PRESS q TO QUIT DEMO") + p.Height = 3 + p.Width = 50 + p.Border.Label = "Text Box" + + strs := []string{"[0] gizak/termui", "[1] editbox.go", "[2] iterrupt.go", "[3] keyboard.go", "[4] output.go", "[5] random_out.go", "[6] dashboard.go", "[7] nsf/termbox-go"} + list := ui.NewList() + list.Items = strs + list.Border.Label = "List" + list.Height = 7 + list.Width = 25 + list.Y = 4 + + g := ui.NewGauge() + g.Percent = 50 + g.Width = 50 + g.Height = 3 + g.Y = 11 + g.Border.Label = "Gauge" + + spark := ui.NewSparkline() + spark.Title = "srv 0:" + spdata := []int{4, 2, 1, 6, 3, 9, 1, 4, 2, 15, 14, 9, 8, 6, 10, 13, 15, 12, 10, 5, 3, 6, 1, 7, 10, 10, 14, 13, 6} + spark.Data = spdata + + spark1 := ui.NewSparkline() + spark1.Title = "srv 1:" + spark1.Data = spdata + + sp := ui.NewSparklines(spark, spark1) + sp.Width = 25 + sp.Height = 7 + sp.Border.Label = "Sparkline" + sp.Y = 4 + sp.X = 25 + + lc := ui.NewLineChart() + sinps := (func() []float64 { + n := 100 + ps := make([]float64, n) + for i := range ps { + ps[i] = 1 + math.Sin(float64(i)/4) + } + return ps + })() + + lc.Border.Label = "Line Chart" + lc.Data = sinps + lc.Width = 50 + lc.Height = 11 + lc.X = 0 + lc.Y = 14 + lc.Mode = "dot" + + bc := ui.NewBarChart() + bcdata := []int{3, 2, 5, 3, 9, 5, 3, 2, 5, 8, 3, 2, 4, 5, 3, 2, 5, 7, 5, 3, 2, 6, 7, 4, 6, 3, 6, 7, 8, 3, 6, 4, 5, 3, 2, 4, 6, 4, 8, 5, 9, 4, 3, 6, 5, 3, 6} + bclabels := []string{"S0", "S1", "S2", "S3", "S4", "S5"} + bc.Border.Label = "Bar Chart" + bc.Width = 26 + bc.Height = 10 + bc.X = 51 + bc.Y = 0 + bc.DataLabels = bclabels + + lc1 := ui.NewLineChart() + lc1.Border.Label = "Line Chart" + rndwalk := (func() []float64 { + n := 150 + d := make([]float64, n) + for i := 1; i < n; i++ { + if i < 20 { + d[i] = d[i-1] + 0.01 + } + if i > 20 { + d[i] = d[i-1] - 0.05 + } + } + return d + })() + lc1.Data = rndwalk + lc1.Width = 26 + lc1.Height = 11 + lc1.X = 51 + lc1.Y = 14 + + p1 := ui.NewPar("Hey!\nI am a borderless block!") + p1.HasBorder = false + p1.Width = 26 + p1.Height = 2 + p1.X = 52 + p1.Y = 11 + + draw := func(t int) { + g.Percent = t % 101 + list.Items = strs[t%9:] + sp.Lines[0].Data = spdata[t%10:] + sp.Lines[1].Data = spdata[t/2%10:] + lc.Data = sinps[t/2:] + lc1.Data = rndwalk[t:] + bc.Data = bcdata[t/2%10:] + ui.Render(p, list, g, sp, lc, bc, lc1, p1) + } + + evt := ui.EventCh() + i := 0 + for { + select { + case e := <-evt: + if e.Type == ui.EventKey && e.Ch == 'q' { + return + } + default: + draw(i) + i++ + if i == 102 { + return + } + time.Sleep(time.Second / 2) + } + } +} diff --git a/Godeps/_workspace/src/github.com/gizak/termui/example/themedefault.png b/Godeps/_workspace/src/github.com/gizak/termui/example/themedefault.png new file mode 100644 index 0000000..23b574f Binary files /dev/null and b/Godeps/_workspace/src/github.com/gizak/termui/example/themedefault.png differ diff --git a/Godeps/_workspace/src/github.com/gizak/termui/example/themehelloworld.png b/Godeps/_workspace/src/github.com/gizak/termui/example/themehelloworld.png new file mode 100644 index 0000000..eaf4d93 Binary files /dev/null and b/Godeps/_workspace/src/github.com/gizak/termui/example/themehelloworld.png differ diff --git a/Godeps/_workspace/src/github.com/gizak/termui/gauge.go b/Godeps/_workspace/src/github.com/gizak/termui/gauge.go new file mode 100644 index 0000000..986f4f3 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gizak/termui/gauge.go @@ -0,0 +1,113 @@ +// Copyright 2015 Zack Guo . All rights reserved. +// Use of this source code is governed by a MIT license that can +// be found in the LICENSE file. + +package termui + +import ( + "strconv" + "strings" +) + +// Gauge is a progress bar like widget. +// A simple example: +/* + g := termui.NewGauge() + g.Percent = 40 + g.Width = 50 + g.Height = 3 + g.Border.Label = "Slim Gauge" + g.BarColor = termui.ColorRed + g.PercentColor = termui.ColorBlue +*/ + +// Align is the position of the gauge's label. +type Align int + +// All supported positions. +const ( + AlignLeft Align = iota + AlignCenter + AlignRight +) + +type Gauge struct { + Block + Percent int + BarColor Attribute + PercentColor Attribute + Label string + LabelAlign Align +} + +// NewGauge return a new gauge with current theme. +func NewGauge() *Gauge { + g := &Gauge{ + Block: *NewBlock(), + PercentColor: theme.GaugePercent, + BarColor: theme.GaugeBar, + Label: "{{percent}}%", + LabelAlign: AlignCenter, + } + + g.Width = 12 + g.Height = 5 + return g +} + +// Buffer implements Bufferer interface. +func (g *Gauge) Buffer() []Point { + ps := g.Block.Buffer() + + // plot bar + w := g.Percent * g.innerWidth / 100 + for i := 0; i < g.innerHeight; i++ { + for j := 0; j < w; j++ { + p := Point{} + p.X = g.innerX + j + p.Y = g.innerY + i + p.Ch = ' ' + p.Bg = g.BarColor + if p.Bg == ColorDefault { + p.Bg |= AttrReverse + } + ps = append(ps, p) + } + } + + // plot percentage + s := strings.Replace(g.Label, "{{percent}}", strconv.Itoa(g.Percent), -1) + pry := g.innerY + g.innerHeight/2 + rs := str2runes(s) + var pos int + switch g.LabelAlign { + case AlignLeft: + pos = 0 + + case AlignCenter: + pos = (g.innerWidth - strWidth(s)) / 2 + + case AlignRight: + pos = g.innerWidth - strWidth(s) + } + + for i, v := range rs { + p := Point{} + p.X = 1 + pos + i + p.Y = pry + p.Ch = v + p.Fg = g.PercentColor + if w+g.innerX > pos+i { + p.Bg = g.BarColor + if p.Bg == ColorDefault { + p.Bg |= AttrReverse + } + + } else { + p.Bg = g.Block.BgColor + } + + ps = append(ps, p) + } + return g.Block.chopOverflow(ps) +} diff --git a/Godeps/_workspace/src/github.com/gizak/termui/grid.go b/Godeps/_workspace/src/github.com/gizak/termui/grid.go new file mode 100644 index 0000000..5f6e85e --- /dev/null +++ b/Godeps/_workspace/src/github.com/gizak/termui/grid.go @@ -0,0 +1,279 @@ +// Copyright 2015 Zack Guo . All rights reserved. +// Use of this source code is governed by a MIT license that can +// be found in the LICENSE file. + +package termui + +// GridBufferer introduces a Bufferer that can be manipulated by Grid. +type GridBufferer interface { + Bufferer + GetHeight() int + SetWidth(int) + SetX(int) + SetY(int) +} + +// Row builds a layout tree +type Row struct { + Cols []*Row //children + Widget GridBufferer // root + X int + Y int + Width int + Height int + Span int + Offset int +} + +// calculate and set the underlying layout tree's x, y, height and width. +func (r *Row) calcLayout() { + r.assignWidth(r.Width) + r.Height = r.solveHeight() + r.assignX(r.X) + r.assignY(r.Y) +} + +// tell if the node is leaf in the tree. +func (r *Row) isLeaf() bool { + return r.Cols == nil || len(r.Cols) == 0 +} + +func (r *Row) isRenderableLeaf() bool { + return r.isLeaf() && r.Widget != nil +} + +// assign widgets' (and their parent rows') width recursively. +func (r *Row) assignWidth(w int) { + r.SetWidth(w) + + accW := 0 // acc span and offset + calcW := make([]int, len(r.Cols)) // calculated width + calcOftX := make([]int, len(r.Cols)) // computated start position of x + + for i, c := range r.Cols { + accW += c.Span + c.Offset + cw := int(float64(c.Span*r.Width) / 12.0) + + if i >= 1 { + calcOftX[i] = calcOftX[i-1] + + calcW[i-1] + + int(float64(r.Cols[i-1].Offset*r.Width)/12.0) + } + + // use up the space if it is the last col + if i == len(r.Cols)-1 && accW == 12 { + cw = r.Width - calcOftX[i] + } + calcW[i] = cw + r.Cols[i].assignWidth(cw) + } +} + +// bottom up calc and set rows' (and their widgets') height, +// return r's total height. +func (r *Row) solveHeight() int { + if r.isRenderableLeaf() { + r.Height = r.Widget.GetHeight() + return r.Widget.GetHeight() + } + + maxh := 0 + if !r.isLeaf() { + for _, c := range r.Cols { + nh := c.solveHeight() + // when embed rows in Cols, row widgets stack up + if r.Widget != nil { + nh += r.Widget.GetHeight() + } + if nh > maxh { + maxh = nh + } + } + } + + r.Height = maxh + return maxh +} + +// recursively assign x position for r tree. +func (r *Row) assignX(x int) { + r.SetX(x) + + if !r.isLeaf() { + acc := 0 + for i, c := range r.Cols { + if c.Offset != 0 { + acc += int(float64(c.Offset*r.Width) / 12.0) + } + r.Cols[i].assignX(x + acc) + acc += c.Width + } + } +} + +// recursively assign y position to r. +func (r *Row) assignY(y int) { + r.SetY(y) + + if r.isLeaf() { + return + } + + for i := range r.Cols { + acc := 0 + if r.Widget != nil { + acc = r.Widget.GetHeight() + } + r.Cols[i].assignY(y + acc) + } + +} + +// GetHeight implements GridBufferer interface. +func (r Row) GetHeight() int { + return r.Height +} + +// SetX implements GridBufferer interface. +func (r *Row) SetX(x int) { + r.X = x + if r.Widget != nil { + r.Widget.SetX(x) + } +} + +// SetY implements GridBufferer interface. +func (r *Row) SetY(y int) { + r.Y = y + if r.Widget != nil { + r.Widget.SetY(y) + } +} + +// SetWidth implements GridBufferer interface. +func (r *Row) SetWidth(w int) { + r.Width = w + if r.Widget != nil { + r.Widget.SetWidth(w) + } +} + +// Buffer implements Bufferer interface, +// recursively merge all widgets buffer +func (r *Row) Buffer() []Point { + merged := []Point{} + + if r.isRenderableLeaf() { + return r.Widget.Buffer() + } + + // for those are not leaves but have a renderable widget + if r.Widget != nil { + merged = append(merged, r.Widget.Buffer()...) + } + + // collect buffer from children + if !r.isLeaf() { + for _, c := range r.Cols { + merged = append(merged, c.Buffer()...) + } + } + + return merged +} + +// Grid implements 12 columns system. +// A simple example: +/* + import ui "github.com/gizak/termui" + // init and create widgets... + + // build + ui.Body.AddRows( + ui.NewRow( + ui.NewCol(6, 0, widget0), + ui.NewCol(6, 0, widget1)), + ui.NewRow( + ui.NewCol(3, 0, widget2), + ui.NewCol(3, 0, widget30, widget31, widget32), + ui.NewCol(6, 0, widget4))) + + // calculate layout + ui.Body.Align() + + ui.Render(ui.Body) +*/ +type Grid struct { + Rows []*Row + Width int + X int + Y int + BgColor Attribute +} + +// NewGrid returns *Grid with given rows. +func NewGrid(rows ...*Row) *Grid { + return &Grid{Rows: rows} +} + +// AddRows appends given rows to Grid. +func (g *Grid) AddRows(rs ...*Row) { + g.Rows = append(g.Rows, rs...) +} + +// NewRow creates a new row out of given columns. +func NewRow(cols ...*Row) *Row { + rs := &Row{Span: 12, Cols: cols} + return rs +} + +// NewCol accepts: widgets are LayoutBufferer or widgets is A NewRow. +// Note that if multiple widgets are provided, they will stack up in the col. +func NewCol(span, offset int, widgets ...GridBufferer) *Row { + r := &Row{Span: span, Offset: offset} + + if widgets != nil && len(widgets) == 1 { + wgt := widgets[0] + nw, isRow := wgt.(*Row) + if isRow { + r.Cols = nw.Cols + } else { + r.Widget = wgt + } + return r + } + + r.Cols = []*Row{} + ir := r + for _, w := range widgets { + nr := &Row{Span: 12, Widget: w} + ir.Cols = []*Row{nr} + ir = nr + } + + return r +} + +// Align calculate each rows' layout. +func (g *Grid) Align() { + h := 0 + for _, r := range g.Rows { + r.SetWidth(g.Width) + r.SetX(g.X) + r.SetY(g.Y + h) + r.calcLayout() + h += r.GetHeight() + } +} + +// Buffer implments Bufferer interface. +func (g Grid) Buffer() []Point { + ps := []Point{} + for _, r := range g.Rows { + ps = append(ps, r.Buffer()...) + } + return ps +} + +// Body corresponds to the entire terminal display region. +var Body *Grid diff --git a/Godeps/_workspace/src/github.com/gizak/termui/grid_test.go b/Godeps/_workspace/src/github.com/gizak/termui/grid_test.go new file mode 100644 index 0000000..cdafb20 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gizak/termui/grid_test.go @@ -0,0 +1,98 @@ +// Copyright 2015 Zack Guo . All rights reserved. +// Use of this source code is governed by a MIT license that can +// be found in the LICENSE file. + +package termui + +import ( + "testing" + + "github.com/davecgh/go-spew/spew" +) + +var r *Row + +func TestRowWidth(t *testing.T) { + p0 := NewPar("p0") + p0.Height = 1 + p1 := NewPar("p1") + p1.Height = 1 + p2 := NewPar("p2") + p2.Height = 1 + p3 := NewPar("p3") + p3.Height = 1 + + /* test against tree: + + r + / \ + 0:w 1 + / \ + 10:w 11 + / + 110:w + / + 1100:w + */ + /* + r = &row{ + Span: 12, + Cols: []*row{ + &row{Widget: p0, Span: 6}, + &row{ + Span: 6, + Cols: []*row{ + &row{Widget: p1, Span: 6}, + &row{ + Span: 6, + Cols: []*row{ + &row{ + Span: 12, + Widget: p2, + Cols: []*row{ + &row{Span: 12, Widget: p3}}}}}}}}} + */ + + r = NewRow( + NewCol(6, 0, p0), + NewCol(6, 0, + NewRow( + NewCol(6, 0, p1), + NewCol(6, 0, p2, p3)))) + + r.assignWidth(100) + if r.Width != 100 || + (r.Cols[0].Width) != 50 || + (r.Cols[1].Width) != 50 || + (r.Cols[1].Cols[0].Width) != 25 || + (r.Cols[1].Cols[1].Width) != 25 || + (r.Cols[1].Cols[1].Cols[0].Width) != 25 || + (r.Cols[1].Cols[1].Cols[0].Cols[0].Width) != 25 { + t.Error("assignWidth fails") + } +} + +func TestRowHeight(t *testing.T) { + spew.Dump() + + if (r.solveHeight()) != 2 || + (r.Cols[1].Cols[1].Height) != 2 || + (r.Cols[1].Cols[1].Cols[0].Height) != 2 || + (r.Cols[1].Cols[0].Height) != 1 { + t.Error("solveHeight fails") + } +} + +func TestAssignXY(t *testing.T) { + r.assignX(0) + r.assignY(0) + if (r.Cols[0].X) != 0 || + (r.Cols[1].Cols[0].X) != 50 || + (r.Cols[1].Cols[1].X) != 75 || + (r.Cols[1].Cols[1].Cols[0].X) != 75 || + (r.Cols[1].Cols[0].Y) != 0 || + (r.Cols[1].Cols[1].Cols[0].Y) != 0 || + (r.Cols[1].Cols[1].Cols[0].Cols[0].Y) != 1 { + t.Error("assignXY fails") + } +} diff --git a/Godeps/_workspace/src/github.com/gizak/termui/helper.go b/Godeps/_workspace/src/github.com/gizak/termui/helper.go new file mode 100644 index 0000000..80d8a02 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gizak/termui/helper.go @@ -0,0 +1,66 @@ +// Copyright 2015 Zack Guo . All rights reserved. +// Use of this source code is governed by a MIT license that can +// be found in the LICENSE file. + +package termui + +import tm "github.com/nsf/termbox-go" +import rw "github.com/mattn/go-runewidth" + +/* ---------------Port from termbox-go --------------------- */ + +// Attribute is printable cell's color and style. +type Attribute uint16 + +const ( + ColorDefault Attribute = iota + ColorBlack + ColorRed + ColorGreen + ColorYellow + ColorBlue + ColorMagenta + ColorCyan + ColorWhite +) + +const NumberofColors = 8 //Have a constant that defines number of colors +const ( + AttrBold Attribute = 1 << (iota + 9) + AttrUnderline + AttrReverse +) + +var ( + dot = "…" + dotw = rw.StringWidth(dot) +) + +/* ----------------------- End ----------------------------- */ + +func toTmAttr(x Attribute) tm.Attribute { + return tm.Attribute(x) +} + +func str2runes(s string) []rune { + return []rune(s) +} + +func trimStr2Runes(s string, w int) []rune { + if w <= 0 { + return []rune{} + } + sw := rw.StringWidth(s) + if sw > w { + return []rune(rw.Truncate(s, w, dot)) + } + return str2runes(s) //[]rune(rw.Truncate(s, w, "")) +} + +func strWidth(s string) int { + return rw.StringWidth(s) +} + +func charWidth(ch rune) int { + return rw.RuneWidth(ch) +} diff --git a/Godeps/_workspace/src/github.com/gizak/termui/helper_test.go b/Godeps/_workspace/src/github.com/gizak/termui/helper_test.go new file mode 100644 index 0000000..6d1a561 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gizak/termui/helper_test.go @@ -0,0 +1,58 @@ +// Copyright 2015 Zack Guo . All rights reserved. +// Use of this source code is governed by a MIT license that can +// be found in the LICENSE file. + +package termui + +import ( + "testing" + + "github.com/davecgh/go-spew/spew" +) + +func TestStr2Rune(t *testing.T) { + s := "你好,世界." + rs := str2runes(s) + if len(rs) != 6 { + t.Error() + } +} + +func TestWidth(t *testing.T) { + s0 := "つのだ☆HIRO" + s1 := "11111111111" + spew.Dump(s0) + spew.Dump(s1) + // above not align for setting East Asian Ambiguous to wide!! + + if strWidth(s0) != strWidth(s1) { + t.Error("str len failed") + } + + len1 := []rune{'a', '2', '&', '「', 'オ', '。'} //will false: 'ᆵ', 'ᄚ', 'ᄒ' + for _, v := range len1 { + if charWidth(v) != 1 { + t.Error("len1 failed") + } + } + + len2 := []rune{'漢', '字', '한', '자', '你', '好', 'だ', '。', '%', 's', 'E', 'ョ', '、', 'ヲ'} + for _, v := range len2 { + if charWidth(v) != 2 { + t.Error("len2 failed") + } + } +} + +func TestTrim(t *testing.T) { + s := "つのだ☆HIRO" + if string(trimStr2Runes(s, 10)) != "つのだ☆HI"+dot { + t.Error("trim failed") + } + if string(trimStr2Runes(s, 11)) != "つのだ☆HIRO" { + t.Error("avoid tail trim failed") + } + if string(trimStr2Runes(s, 15)) != "つのだ☆HIRO" { + t.Error("avoid trim failed") + } +} diff --git a/Godeps/_workspace/src/github.com/gizak/termui/list.go b/Godeps/_workspace/src/github.com/gizak/termui/list.go new file mode 100644 index 0000000..0640932 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gizak/termui/list.go @@ -0,0 +1,104 @@ +// Copyright 2015 Zack Guo . All rights reserved. +// Use of this source code is governed by a MIT license that can +// be found in the LICENSE file. + +package termui + +import "strings" + +// List displays []string as its items, +// it has a Overflow option (default is "hidden"), when set to "hidden", +// the item exceeding List's width is truncated, but when set to "wrap", +// the overflowed text breaks into next line. +/* + strs := []string{ + "[0] github.com/gizak/termui", + "[1] editbox.go", + "[2] iterrupt.go", + "[3] keyboard.go", + "[4] output.go", + "[5] random_out.go", + "[6] dashboard.go", + "[7] nsf/termbox-go"} + + ls := termui.NewList() + ls.Items = strs + ls.ItemFgColor = termui.ColorYellow + ls.Border.Label = "List" + ls.Height = 7 + ls.Width = 25 + ls.Y = 0 +*/ +type List struct { + Block + Items []string + Overflow string + ItemFgColor Attribute + ItemBgColor Attribute +} + +// NewList returns a new *List with current theme. +func NewList() *List { + l := &List{Block: *NewBlock()} + l.Overflow = "hidden" + l.ItemFgColor = theme.ListItemFg + l.ItemBgColor = theme.ListItemBg + return l +} + +// Buffer implements Bufferer interface. +func (l *List) Buffer() []Point { + ps := l.Block.Buffer() + switch l.Overflow { + case "wrap": + rs := str2runes(strings.Join(l.Items, "\n")) + i, j, k := 0, 0, 0 + for i < l.innerHeight && k < len(rs) { + w := charWidth(rs[k]) + if rs[k] == '\n' || j+w > l.innerWidth { + i++ + j = 0 + if rs[k] == '\n' { + k++ + } + continue + } + pi := Point{} + pi.X = l.innerX + j + pi.Y = l.innerY + i + + pi.Ch = rs[k] + pi.Bg = l.ItemBgColor + pi.Fg = l.ItemFgColor + + ps = append(ps, pi) + k++ + j++ + } + + case "hidden": + trimItems := l.Items + if len(trimItems) > l.innerHeight { + trimItems = trimItems[:l.innerHeight] + } + for i, v := range trimItems { + rs := trimStr2Runes(v, l.innerWidth) + + j := 0 + for _, vv := range rs { + w := charWidth(vv) + p := Point{} + p.X = l.innerX + j + p.Y = l.innerY + i + + p.Ch = vv + p.Bg = l.ItemBgColor + p.Fg = l.ItemFgColor + + ps = append(ps, p) + j += w + } + } + } + return l.Block.chopOverflow(ps) +} diff --git a/Godeps/_workspace/src/github.com/gizak/termui/mbar.go b/Godeps/_workspace/src/github.com/gizak/termui/mbar.go new file mode 100644 index 0000000..9d18c2c --- /dev/null +++ b/Godeps/_workspace/src/github.com/gizak/termui/mbar.go @@ -0,0 +1,233 @@ +// Copyright 2015 Zack Guo . All rights reserved. +// Use of this source code is governed by a MIT license that can +// be found in the LICENSE file. + +package termui + +import ( + "fmt" +) + +// This is the implemetation of multi-colored or stacked bar graph. This is different from default barGraph which is implemented in bar.go +// Multi-Colored-BarChart creates multiple bars in a widget: +/* + bc := termui.NewMBarChart() + data := make([][]int, 2) + data[0] := []int{3, 2, 5, 7, 9, 4} + data[1] := []int{7, 8, 5, 3, 1, 6} + bclabels := []string{"S0", "S1", "S2", "S3", "S4", "S5"} + bc.Border.Label = "Bar Chart" + bc.Data = data + bc.Width = 26 + bc.Height = 10 + bc.DataLabels = bclabels + bc.TextColor = termui.ColorGreen + bc.BarColor = termui.ColorRed + bc.NumColor = termui.ColorYellow +*/ +type MBarChart struct { + Block + BarColor [NumberofColors]Attribute + TextColor Attribute + NumColor [NumberofColors]Attribute + Data [NumberofColors][]int + DataLabels []string + BarWidth int + BarGap int + labels [][]rune + dataNum [NumberofColors][][]rune + numBar int + scale float64 + max int + minDataLen int + numStack int + ShowScale bool + maxScale []rune +} + +// NewBarChart returns a new *BarChart with current theme. +func NewMBarChart() *MBarChart { + bc := &MBarChart{Block: *NewBlock()} + bc.BarColor[0] = theme.MBarChartBar + bc.NumColor[0] = theme.MBarChartNum + bc.TextColor = theme.MBarChartText + bc.BarGap = 1 + bc.BarWidth = 3 + return bc +} + +func (bc *MBarChart) layout() { + bc.numBar = bc.innerWidth / (bc.BarGap + bc.BarWidth) + bc.labels = make([][]rune, bc.numBar) + DataLen := 0 + LabelLen := len(bc.DataLabels) + bc.minDataLen = 9999 //Set this to some very hight value so that we find the minimum one We want to know which array among data[][] has got the least length + + // We need to know how many stack/data array data[0] , data[1] are there + for i := 0; i < len(bc.Data); i++ { + if bc.Data[i] == nil { + break + } + DataLen++ + } + bc.numStack = DataLen + + //We need to know what is the mimimum size of data array data[0] could have 10 elements data[1] could have only 5, so we plot only 5 bar graphs + + for i := 0; i < DataLen; i++ { + if bc.minDataLen > len(bc.Data[i]) { + bc.minDataLen = len(bc.Data[i]) + } + } + + if LabelLen > bc.minDataLen { + LabelLen = bc.minDataLen + } + + for i := 0; i < LabelLen && i < bc.numBar; i++ { + bc.labels[i] = trimStr2Runes(bc.DataLabels[i], bc.BarWidth) + } + + for i := 0; i < bc.numStack; i++ { + bc.dataNum[i] = make([][]rune, len(bc.Data[i])) + //For each stack of bar calcualte the rune + for j := 0; j < LabelLen && i < bc.numBar; j++ { + n := bc.Data[i][j] + s := fmt.Sprint(n) + bc.dataNum[i][j] = trimStr2Runes(s, bc.BarWidth) + } + //If color is not defined by default then populate a color that is different from the prevous bar + if bc.BarColor[i] == ColorDefault && bc.NumColor[i] == ColorDefault { + if i == 0 { + bc.BarColor[i] = ColorBlack + } else { + bc.BarColor[i] = bc.BarColor[i-1] + 1 + if bc.BarColor[i] > NumberofColors { + bc.BarColor[i] = ColorBlack + } + } + bc.NumColor[i] = (NumberofColors + 1) - bc.BarColor[i] //Make NumColor opposite of barColor for visibility + } + } + + //If Max value is not set then we have to populate, this time the max value will be max(sum(d1[0],d2[0],d3[0]) .... sum(d1[n], d2[n], d3[n])) + + if bc.max == 0 { + bc.max = -1 + } + for i := 0; i < bc.minDataLen && i < LabelLen; i++ { + var dsum int + for j := 0; j < bc.numStack; j++ { + dsum += bc.Data[j][i] + } + if dsum > bc.max { + bc.max = dsum + } + } + + //Finally Calculate max sale + if bc.ShowScale { + s := fmt.Sprintf("%d", bc.max) + bc.maxScale = trimStr2Runes(s, len(s)) + bc.scale = float64(bc.max) / float64(bc.innerHeight-2) + } else { + bc.scale = float64(bc.max) / float64(bc.innerHeight-1) + } + +} + +func (bc *MBarChart) SetMax(max int) { + + if max > 0 { + bc.max = max + } +} + +// Buffer implements Bufferer interface. +func (bc *MBarChart) Buffer() []Point { + ps := bc.Block.Buffer() + bc.layout() + var oftX int + + for i := 0; i < bc.numBar && i < bc.minDataLen && i < len(bc.DataLabels); i++ { + ph := 0 //Previous Height to stack up + oftX = i * (bc.BarWidth + bc.BarGap) + for i1 := 0; i1 < bc.numStack; i1++ { + h := int(float64(bc.Data[i1][i]) / bc.scale) + // plot bars + for j := 0; j < bc.BarWidth; j++ { + for k := 0; k < h; k++ { + p := Point{} + p.Ch = ' ' + p.Bg = bc.BarColor[i1] + if bc.BarColor[i1] == ColorDefault { // when color is default, space char treated as transparent! + p.Bg |= AttrReverse + } + p.X = bc.innerX + i*(bc.BarWidth+bc.BarGap) + j + p.Y = bc.innerY + bc.innerHeight - 2 - k - ph + ps = append(ps, p) + } + } + ph += h + } + // plot text + for j, k := 0, 0; j < len(bc.labels[i]); j++ { + w := charWidth(bc.labels[i][j]) + p := Point{} + p.Ch = bc.labels[i][j] + p.Bg = bc.BgColor + p.Fg = bc.TextColor + p.Y = bc.innerY + bc.innerHeight - 1 + p.X = bc.innerX + oftX + ((bc.BarWidth - len(bc.labels[i])) / 2) + k + ps = append(ps, p) + k += w + } + // plot num + ph = 0 //re-initialize previous height + for i1 := 0; i1 < bc.numStack; i1++ { + h := int(float64(bc.Data[i1][i]) / bc.scale) + for j := 0; j < len(bc.dataNum[i1][i]) && h > 0; j++ { + p := Point{} + p.Ch = bc.dataNum[i1][i][j] + p.Fg = bc.NumColor[i1] + p.Bg = bc.BarColor[i1] + if bc.BarColor[i1] == ColorDefault { // the same as above + p.Bg |= AttrReverse + } + if h == 0 { + p.Bg = bc.BgColor + } + p.X = bc.innerX + oftX + (bc.BarWidth-len(bc.dataNum[i1][i]))/2 + j + p.Y = bc.innerY + bc.innerHeight - 2 - ph + ps = append(ps, p) + } + ph += h + } + } + + if bc.ShowScale { + //Currently bar graph only supprts data range from 0 to MAX + //Plot 0 + p := Point{} + p.Ch = '0' + p.Bg = bc.BgColor + p.Fg = bc.TextColor + p.Y = bc.innerY + bc.innerHeight - 2 + p.X = bc.X + ps = append(ps, p) + + //Plot the maximum sacle value + for i := 0; i < len(bc.maxScale); i++ { + p := Point{} + p.Ch = bc.maxScale[i] + p.Bg = bc.BgColor + p.Fg = bc.TextColor + p.Y = bc.innerY + p.X = bc.X + i + ps = append(ps, p) + } + + } + + return bc.Block.chopOverflow(ps) +} diff --git a/Godeps/_workspace/src/github.com/gizak/termui/p.go b/Godeps/_workspace/src/github.com/gizak/termui/p.go new file mode 100644 index 0000000..e327d74 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gizak/termui/p.go @@ -0,0 +1,71 @@ +// Copyright 2015 Zack Guo . All rights reserved. +// Use of this source code is governed by a MIT license that can +// be found in the LICENSE file. + +package termui + +// Par displays a paragraph. +/* + par := termui.NewPar("Simple Text") + par.Height = 3 + par.Width = 17 + par.Border.Label = "Label" +*/ +type Par struct { + Block + Text string + TextFgColor Attribute + TextBgColor Attribute +} + +// NewPar returns a new *Par with given text as its content. +func NewPar(s string) *Par { + return &Par{ + Block: *NewBlock(), + Text: s, + TextFgColor: theme.ParTextFg, + TextBgColor: theme.ParTextBg} +} + +// Buffer implements Bufferer interface. +func (p *Par) Buffer() []Point { + ps := p.Block.Buffer() + + rs := str2runes(p.Text) + i, j, k := 0, 0, 0 + for i < p.innerHeight && k < len(rs) { + // the width of char is about to print + w := charWidth(rs[k]) + + if rs[k] == '\n' || j+w > p.innerWidth { + i++ + j = 0 // set x = 0 + if rs[k] == '\n' { + k++ + } + + if i >= p.innerHeight { + ps = append(ps, newPointWithAttrs('…', + p.innerX+p.innerWidth-1, + p.innerY+p.innerHeight-1, + p.TextFgColor, p.TextBgColor)) + break + } + + continue + } + pi := Point{} + pi.X = p.innerX + j + pi.Y = p.innerY + i + + pi.Ch = rs[k] + pi.Bg = p.TextBgColor + pi.Fg = p.TextFgColor + + ps = append(ps, pi) + + k++ + j += w + } + return p.Block.chopOverflow(ps) +} diff --git a/Godeps/_workspace/src/github.com/gizak/termui/point.go b/Godeps/_workspace/src/github.com/gizak/termui/point.go new file mode 100644 index 0000000..c381af9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gizak/termui/point.go @@ -0,0 +1,28 @@ +// Copyright 2015 Zack Guo . All rights reserved. +// Use of this source code is governed by a MIT license that can +// be found in the LICENSE file. + +package termui + +// Point stands for a single cell in terminal. +type Point struct { + Ch rune + Bg Attribute + Fg Attribute + X int + Y int +} + +func newPoint(c rune, x, y int) (p Point) { + p.Ch = c + p.X = x + p.Y = y + return +} + +func newPointWithAttrs(c rune, x, y int, fg, bg Attribute) Point { + p := newPoint(c, x, y) + p.Bg = bg + p.Fg = fg + return p +} diff --git a/Godeps/_workspace/src/github.com/gizak/termui/render.go b/Godeps/_workspace/src/github.com/gizak/termui/render.go new file mode 100644 index 0000000..d697d0a --- /dev/null +++ b/Godeps/_workspace/src/github.com/gizak/termui/render.go @@ -0,0 +1,60 @@ +// Copyright 2015 Zack Guo . All rights reserved. +// Use of this source code is governed by a MIT license that can +// be found in the LICENSE file. + +package termui + +import tm "github.com/nsf/termbox-go" + +// Bufferer should be implemented by all renderable components. +type Bufferer interface { + Buffer() []Point +} + +// Init initializes termui library. This function should be called before any others. +// After initialization, the library must be finalized by 'Close' function. +func Init() error { + Body = NewGrid() + Body.X = 0 + Body.Y = 0 + Body.BgColor = theme.BodyBg + defer func() { + w, _ := tm.Size() + Body.Width = w + evtListen() + }() + return tm.Init() +} + +// Close finalizes termui library, +// should be called after successful initialization when termui's functionality isn't required anymore. +func Close() { + tm.Close() +} + +// TermWidth returns the current terminal's width. +func TermWidth() int { + tm.Sync() + w, _ := tm.Size() + return w +} + +// TermHeight returns the current terminal's height. +func TermHeight() int { + tm.Sync() + _, h := tm.Size() + return h +} + +// Render renders all Bufferer in the given order from left to right, +// right could overlap on left ones. +func Render(rs ...Bufferer) { + tm.Clear(tm.ColorDefault, toTmAttr(theme.BodyBg)) + for _, r := range rs { + buf := r.Buffer() + for _, v := range buf { + tm.SetCell(v.X, v.Y, v.Ch, toTmAttr(v.Fg), toTmAttr(v.Bg)) + } + } + tm.Flush() +} diff --git a/Godeps/_workspace/src/github.com/gizak/termui/sparkline.go b/Godeps/_workspace/src/github.com/gizak/termui/sparkline.go new file mode 100644 index 0000000..c63a585 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gizak/termui/sparkline.go @@ -0,0 +1,156 @@ +// Copyright 2015 Zack Guo . All rights reserved. +// Use of this source code is governed by a MIT license that can +// be found in the LICENSE file. + +package termui + +import "math" + +// Sparkline is like: ▅▆▂▂▅▇▂▂▃▆▆▆▅▃ +/* + data := []int{4, 2, 1, 6, 3, 9, 1, 4, 2, 15, 14, 9, 8, 6, 10, 13, 15, 12, 10, 5, 3, 6, 1} + spl := termui.NewSparkline() + spl.Data = data + spl.Title = "Sparkline 0" + spl.LineColor = termui.ColorGreen +*/ +type Sparkline struct { + Data []int + Height int + Title string + TitleColor Attribute + LineColor Attribute + displayHeight int + scale float32 + max int +} + +// Sparklines is a renderable widget which groups together the given sparklines. +/* + spls := termui.NewSparklines(spl0,spl1,spl2) //... + spls.Height = 2 + spls.Width = 20 +*/ +type Sparklines struct { + Block + Lines []Sparkline + displayLines int + displayWidth int +} + +var sparks = []rune{'▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'} + +// Add appends a given Sparkline to s *Sparklines. +func (s *Sparklines) Add(sl Sparkline) { + s.Lines = append(s.Lines, sl) +} + +// NewSparkline returns a unrenderable single sparkline that intended to be added into Sparklines. +func NewSparkline() Sparkline { + return Sparkline{ + Height: 1, + TitleColor: theme.SparklineTitle, + LineColor: theme.SparklineLine} +} + +// NewSparklines return a new *Spaklines with given Sparkline(s), you can always add a new Sparkline later. +func NewSparklines(ss ...Sparkline) *Sparklines { + s := &Sparklines{Block: *NewBlock(), Lines: ss} + return s +} + +func (sl *Sparklines) update() { + for i, v := range sl.Lines { + if v.Title == "" { + sl.Lines[i].displayHeight = v.Height + } else { + sl.Lines[i].displayHeight = v.Height + 1 + } + } + sl.displayWidth = sl.innerWidth + + // get how many lines gotta display + h := 0 + sl.displayLines = 0 + for _, v := range sl.Lines { + if h+v.displayHeight <= sl.innerHeight { + sl.displayLines++ + } else { + break + } + h += v.displayHeight + } + + for i := 0; i < sl.displayLines; i++ { + data := sl.Lines[i].Data + + max := math.MinInt32 + for _, v := range data { + if max < v { + max = v + } + } + sl.Lines[i].max = max + sl.Lines[i].scale = float32(8*sl.Lines[i].Height) / float32(max) + } +} + +// Buffer implements Bufferer interface. +func (sl *Sparklines) Buffer() []Point { + ps := sl.Block.Buffer() + sl.update() + + oftY := 0 + for i := 0; i < sl.displayLines; i++ { + l := sl.Lines[i] + data := l.Data + + if len(data) > sl.innerWidth { + data = data[len(data)-sl.innerWidth:] + } + + if l.Title != "" { + rs := trimStr2Runes(l.Title, sl.innerWidth) + oftX := 0 + for _, v := range rs { + w := charWidth(v) + p := Point{} + p.Ch = v + p.Fg = l.TitleColor + p.Bg = sl.BgColor + p.X = sl.innerX + oftX + p.Y = sl.innerY + oftY + ps = append(ps, p) + oftX += w + } + } + + for j, v := range data { + h := int(float32(v)*l.scale + 0.5) + barCnt := h / 8 + barMod := h % 8 + for jj := 0; jj < barCnt; jj++ { + p := Point{} + p.X = sl.innerX + j + p.Y = sl.innerY + oftY + l.Height - jj + p.Ch = ' ' // => sparks[7] + p.Bg = l.LineColor + //p.Bg = sl.BgColor + ps = append(ps, p) + } + if barMod != 0 { + p := Point{} + p.X = sl.innerX + j + p.Y = sl.innerY + oftY + l.Height - barCnt + p.Ch = sparks[barMod-1] + p.Fg = l.LineColor + p.Bg = sl.BgColor + ps = append(ps, p) + } + } + + oftY += l.displayHeight + } + + return sl.Block.chopOverflow(ps) +} diff --git a/Godeps/_workspace/src/github.com/gizak/termui/theme.go b/Godeps/_workspace/src/github.com/gizak/termui/theme.go new file mode 100644 index 0000000..c8ad947 --- /dev/null +++ b/Godeps/_workspace/src/github.com/gizak/termui/theme.go @@ -0,0 +1,84 @@ +// Copyright 2015 Zack Guo . All rights reserved. +// Use of this source code is governed by a MIT license that can +// be found in the LICENSE file. + +package termui + +// A ColorScheme represents the current look-and-feel of the dashboard. +type ColorScheme struct { + BodyBg Attribute + BlockBg Attribute + HasBorder bool + BorderFg Attribute + BorderBg Attribute + BorderLabelTextFg Attribute + BorderLabelTextBg Attribute + ParTextFg Attribute + ParTextBg Attribute + SparklineLine Attribute + SparklineTitle Attribute + GaugeBar Attribute + GaugePercent Attribute + LineChartLine Attribute + LineChartAxes Attribute + ListItemFg Attribute + ListItemBg Attribute + BarChartBar Attribute + BarChartText Attribute + BarChartNum Attribute + MBarChartBar Attribute + MBarChartText Attribute + MBarChartNum Attribute +} + +// default color scheme depends on the user's terminal setting. +var themeDefault = ColorScheme{HasBorder: true} + +var themeHelloWorld = ColorScheme{ + BodyBg: ColorBlack, + BlockBg: ColorBlack, + HasBorder: true, + BorderFg: ColorWhite, + BorderBg: ColorBlack, + BorderLabelTextBg: ColorBlack, + BorderLabelTextFg: ColorGreen, + ParTextBg: ColorBlack, + ParTextFg: ColorWhite, + SparklineLine: ColorMagenta, + SparklineTitle: ColorWhite, + GaugeBar: ColorRed, + GaugePercent: ColorWhite, + LineChartLine: ColorYellow | AttrBold, + LineChartAxes: ColorWhite, + ListItemBg: ColorBlack, + ListItemFg: ColorYellow, + BarChartBar: ColorRed, + BarChartNum: ColorWhite, + BarChartText: ColorCyan, + MBarChartBar: ColorRed, + MBarChartNum: ColorWhite, + MBarChartText: ColorCyan, +} + +var theme = themeDefault // global dep + +// Theme returns the currently used theme. +func Theme() ColorScheme { + return theme +} + +// SetTheme sets a new, custom theme. +func SetTheme(newTheme ColorScheme) { + theme = newTheme +} + +// UseTheme sets a predefined scheme. Currently available: "hello-world" and +// "black-and-white". +func UseTheme(th string) { + switch th { + case "helloworld": + theme = themeHelloWorld + default: + theme = themeDefault + } +} diff --git a/Godeps/_workspace/src/github.com/mattn/go-runewidth/.travis.yml b/Godeps/_workspace/src/github.com/mattn/go-runewidth/.travis.yml new file mode 100644 index 0000000..ad584f4 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mattn/go-runewidth/.travis.yml @@ -0,0 +1,9 @@ +language: go +go: + - tip +before_install: + - go get github.com/axw/gocov/gocov + - go get github.com/mattn/goveralls + - go get golang.org/x/tools/cmd/cover +script: + - $HOME/gopath/bin/goveralls -repotoken lAKAWPzcGsD3A8yBX3BGGtRUdJ6CaGERL diff --git a/Godeps/_workspace/src/github.com/mattn/go-runewidth/README.mkd b/Godeps/_workspace/src/github.com/mattn/go-runewidth/README.mkd new file mode 100644 index 0000000..4f0d583 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mattn/go-runewidth/README.mkd @@ -0,0 +1,25 @@ +go-runewidth +============ + +[![Build Status](https://travis-ci.org/mattn/go-runewidth.png?branch=master)](https://travis-ci.org/mattn/go-runewidth) +[![Coverage Status](https://coveralls.io/repos/mattn/go-runewidth/badge.png?branch=HEAD)](https://coveralls.io/r/mattn/go-runewidth?branch=HEAD) + +Provides functions to get fixed width of the character or string. + +Usage +----- + +```go +runewidth.StringWidth("つのだ☆HIRO") == 12 +``` + + +Author +------ + +Yasuhiro Matsumoto + +License +------- + +under the MIT License: http://mattn.mit-license.org/2013 diff --git a/Godeps/_workspace/src/github.com/mattn/go-runewidth/runewidth.go b/Godeps/_workspace/src/github.com/mattn/go-runewidth/runewidth.go new file mode 100644 index 0000000..1050395 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mattn/go-runewidth/runewidth.go @@ -0,0 +1,207 @@ +package runewidth + +var EastAsianWidth = IsEastAsian() +var DefaultCondition = &Condition{EastAsianWidth} + +type interval struct { + first rune + last rune +} + +var combining = []interval{ + {0x0300, 0x036F}, {0x0483, 0x0486}, {0x0488, 0x0489}, + {0x0591, 0x05BD}, {0x05BF, 0x05BF}, {0x05C1, 0x05C2}, + {0x05C4, 0x05C5}, {0x05C7, 0x05C7}, {0x0600, 0x0603}, + {0x0610, 0x0615}, {0x064B, 0x065E}, {0x0670, 0x0670}, + {0x06D6, 0x06E4}, {0x06E7, 0x06E8}, {0x06EA, 0x06ED}, + {0x070F, 0x070F}, {0x0711, 0x0711}, {0x0730, 0x074A}, + {0x07A6, 0x07B0}, {0x07EB, 0x07F3}, {0x0901, 0x0902}, + {0x093C, 0x093C}, {0x0941, 0x0948}, {0x094D, 0x094D}, + {0x0951, 0x0954}, {0x0962, 0x0963}, {0x0981, 0x0981}, + {0x09BC, 0x09BC}, {0x09C1, 0x09C4}, {0x09CD, 0x09CD}, + {0x09E2, 0x09E3}, {0x0A01, 0x0A02}, {0x0A3C, 0x0A3C}, + {0x0A41, 0x0A42}, {0x0A47, 0x0A48}, {0x0A4B, 0x0A4D}, + {0x0A70, 0x0A71}, {0x0A81, 0x0A82}, {0x0ABC, 0x0ABC}, + {0x0AC1, 0x0AC5}, {0x0AC7, 0x0AC8}, {0x0ACD, 0x0ACD}, + {0x0AE2, 0x0AE3}, {0x0B01, 0x0B01}, {0x0B3C, 0x0B3C}, + {0x0B3F, 0x0B3F}, {0x0B41, 0x0B43}, {0x0B4D, 0x0B4D}, + {0x0B56, 0x0B56}, {0x0B82, 0x0B82}, {0x0BC0, 0x0BC0}, + {0x0BCD, 0x0BCD}, {0x0C3E, 0x0C40}, {0x0C46, 0x0C48}, + {0x0C4A, 0x0C4D}, {0x0C55, 0x0C56}, {0x0CBC, 0x0CBC}, + {0x0CBF, 0x0CBF}, {0x0CC6, 0x0CC6}, {0x0CCC, 0x0CCD}, + {0x0CE2, 0x0CE3}, {0x0D41, 0x0D43}, {0x0D4D, 0x0D4D}, + {0x0DCA, 0x0DCA}, {0x0DD2, 0x0DD4}, {0x0DD6, 0x0DD6}, + {0x0E31, 0x0E31}, {0x0E34, 0x0E3A}, {0x0E47, 0x0E4E}, + {0x0EB1, 0x0EB1}, {0x0EB4, 0x0EB9}, {0x0EBB, 0x0EBC}, + {0x0EC8, 0x0ECD}, {0x0F18, 0x0F19}, {0x0F35, 0x0F35}, + {0x0F37, 0x0F37}, {0x0F39, 0x0F39}, {0x0F71, 0x0F7E}, + {0x0F80, 0x0F84}, {0x0F86, 0x0F87}, {0x0F90, 0x0F97}, + {0x0F99, 0x0FBC}, {0x0FC6, 0x0FC6}, {0x102D, 0x1030}, + {0x1032, 0x1032}, {0x1036, 0x1037}, {0x1039, 0x1039}, + {0x1058, 0x1059}, {0x1160, 0x11FF}, {0x135F, 0x135F}, + {0x1712, 0x1714}, {0x1732, 0x1734}, {0x1752, 0x1753}, + {0x1772, 0x1773}, {0x17B4, 0x17B5}, {0x17B7, 0x17BD}, + {0x17C6, 0x17C6}, {0x17C9, 0x17D3}, {0x17DD, 0x17DD}, + {0x180B, 0x180D}, {0x18A9, 0x18A9}, {0x1920, 0x1922}, + {0x1927, 0x1928}, {0x1932, 0x1932}, {0x1939, 0x193B}, + {0x1A17, 0x1A18}, {0x1B00, 0x1B03}, {0x1B34, 0x1B34}, + {0x1B36, 0x1B3A}, {0x1B3C, 0x1B3C}, {0x1B42, 0x1B42}, + {0x1B6B, 0x1B73}, {0x1DC0, 0x1DCA}, {0x1DFE, 0x1DFF}, + {0x200B, 0x200F}, {0x202A, 0x202E}, {0x2060, 0x2063}, + {0x206A, 0x206F}, {0x20D0, 0x20EF}, {0x302A, 0x302F}, + {0x3099, 0x309A}, {0xA806, 0xA806}, {0xA80B, 0xA80B}, + {0xA825, 0xA826}, {0xFB1E, 0xFB1E}, {0xFE00, 0xFE0F}, + {0xFE20, 0xFE23}, {0xFEFF, 0xFEFF}, {0xFFF9, 0xFFFB}, + {0x10A01, 0x10A03}, {0x10A05, 0x10A06}, {0x10A0C, 0x10A0F}, + {0x10A38, 0x10A3A}, {0x10A3F, 0x10A3F}, {0x1D167, 0x1D169}, + {0x1D173, 0x1D182}, {0x1D185, 0x1D18B}, {0x1D1AA, 0x1D1AD}, + {0x1D242, 0x1D244}, {0xE0001, 0xE0001}, {0xE0020, 0xE007F}, + {0xE0100, 0xE01EF}, +} + +var ambiguous = []interval{ + {0x00A1, 0x00A1}, {0x00A4, 0x00A4}, {0x00A7, 0x00A8}, + {0x00AA, 0x00AA}, {0x00AE, 0x00AE}, {0x00B0, 0x00B4}, + {0x00B6, 0x00BA}, {0x00BC, 0x00BF}, {0x00C6, 0x00C6}, + {0x00D0, 0x00D0}, {0x00D7, 0x00D8}, {0x00DE, 0x00E1}, + {0x00E6, 0x00E6}, {0x00E8, 0x00EA}, {0x00EC, 0x00ED}, + {0x00F0, 0x00F0}, {0x00F2, 0x00F3}, {0x00F7, 0x00FA}, + {0x00FC, 0x00FC}, {0x00FE, 0x00FE}, {0x0101, 0x0101}, + {0x0111, 0x0111}, {0x0113, 0x0113}, {0x011B, 0x011B}, + {0x0126, 0x0127}, {0x012B, 0x012B}, {0x0131, 0x0133}, + {0x0138, 0x0138}, {0x013F, 0x0142}, {0x0144, 0x0144}, + {0x0148, 0x014B}, {0x014D, 0x014D}, {0x0152, 0x0153}, + {0x0166, 0x0167}, {0x016B, 0x016B}, {0x01CE, 0x01CE}, + {0x01D0, 0x01D0}, {0x01D2, 0x01D2}, {0x01D4, 0x01D4}, + {0x01D6, 0x01D6}, {0x01D8, 0x01D8}, {0x01DA, 0x01DA}, + {0x01DC, 0x01DC}, {0x0251, 0x0251}, {0x0261, 0x0261}, + {0x02C4, 0x02C4}, {0x02C7, 0x02C7}, {0x02C9, 0x02CB}, + {0x02CD, 0x02CD}, {0x02D0, 0x02D0}, {0x02D8, 0x02DB}, + {0x02DD, 0x02DD}, {0x02DF, 0x02DF}, {0x0391, 0x03A1}, + {0x03A3, 0x03A9}, {0x03B1, 0x03C1}, {0x03C3, 0x03C9}, + {0x0401, 0x0401}, {0x0410, 0x044F}, {0x0451, 0x0451}, + {0x2010, 0x2010}, {0x2013, 0x2016}, {0x2018, 0x2019}, + {0x201C, 0x201D}, {0x2020, 0x2022}, {0x2024, 0x2027}, + {0x2030, 0x2030}, {0x2032, 0x2033}, {0x2035, 0x2035}, + {0x203B, 0x203B}, {0x203E, 0x203E}, {0x2074, 0x2074}, + {0x207F, 0x207F}, {0x2081, 0x2084}, {0x20AC, 0x20AC}, + {0x2103, 0x2103}, {0x2105, 0x2105}, {0x2109, 0x2109}, + {0x2113, 0x2113}, {0x2116, 0x2116}, {0x2121, 0x2122}, + {0x2126, 0x2126}, {0x212B, 0x212B}, {0x2153, 0x2154}, + {0x215B, 0x215E}, {0x2160, 0x216B}, {0x2170, 0x2179}, + {0x2190, 0x2199}, {0x21B8, 0x21B9}, {0x21D2, 0x21D2}, + {0x21D4, 0x21D4}, {0x21E7, 0x21E7}, {0x2200, 0x2200}, + {0x2202, 0x2203}, {0x2207, 0x2208}, {0x220B, 0x220B}, + {0x220F, 0x220F}, {0x2211, 0x2211}, {0x2215, 0x2215}, + {0x221A, 0x221A}, {0x221D, 0x2220}, {0x2223, 0x2223}, + {0x2225, 0x2225}, {0x2227, 0x222C}, {0x222E, 0x222E}, + {0x2234, 0x2237}, {0x223C, 0x223D}, {0x2248, 0x2248}, + {0x224C, 0x224C}, {0x2252, 0x2252}, {0x2260, 0x2261}, + {0x2264, 0x2267}, {0x226A, 0x226B}, {0x226E, 0x226F}, + {0x2282, 0x2283}, {0x2286, 0x2287}, {0x2295, 0x2295}, + {0x2299, 0x2299}, {0x22A5, 0x22A5}, {0x22BF, 0x22BF}, + {0x2312, 0x2312}, {0x2460, 0x24E9}, {0x24EB, 0x254B}, + {0x2550, 0x2573}, {0x2580, 0x258F}, {0x2592, 0x2595}, + {0x25A0, 0x25A1}, {0x25A3, 0x25A9}, {0x25B2, 0x25B3}, + {0x25B6, 0x25B7}, {0x25BC, 0x25BD}, {0x25C0, 0x25C1}, + {0x25C6, 0x25C8}, {0x25CB, 0x25CB}, {0x25CE, 0x25D1}, + {0x25E2, 0x25E5}, {0x25EF, 0x25EF}, {0x2605, 0x2606}, + {0x2609, 0x2609}, {0x260E, 0x260F}, {0x2614, 0x2615}, + {0x261C, 0x261C}, {0x261E, 0x261E}, {0x2640, 0x2640}, + {0x2642, 0x2642}, {0x2660, 0x2661}, {0x2663, 0x2665}, + {0x2667, 0x266A}, {0x266C, 0x266D}, {0x266F, 0x266F}, + {0x273D, 0x273D}, {0x2776, 0x277F}, {0xE000, 0xF8FF}, + {0xFFFD, 0xFFFD}, {0xF0000, 0xFFFFD}, {0x100000, 0x10FFFD}, +} + +type Condition struct { + EastAsianWidth bool +} + +func NewCondition() *Condition { + return &Condition{EastAsianWidth} +} + +// RuneWidth returns the number of cells in r. +// See http://www.unicode.org/reports/tr11/ +func (c *Condition) RuneWidth(r rune) int { + if r == 0 { + return 0 + } + if r < 32 || (r >= 0x7f && r < 0xa0) { + return 1 + } + for _, iv := range combining { + if iv.first <= r && r <= iv.last { + return 0 + } + } + + if c.EastAsianWidth && IsAmbiguousWidth(r) { + return 2 + } + + if r >= 0x1100 && + (r <= 0x115f || r == 0x2329 || r == 0x232a || + (r >= 0x2e80 && r <= 0xa4cf && r != 0x303f) || + (r >= 0xac00 && r <= 0xd7a3) || + (r >= 0xf900 && r <= 0xfaff) || + (r >= 0xfe30 && r <= 0xfe6f) || + (r >= 0xff00 && r <= 0xff60) || + (r >= 0xffe0 && r <= 0xffe6) || + (r >= 0x20000 && r <= 0x2fffd) || + (r >= 0x30000 && r <= 0x3fffd)) { + return 2 + } + return 1 +} + +func (c *Condition) StringWidth(s string) (width int) { + for _, r := range []rune(s) { + width += c.RuneWidth(r) + } + return width +} + +func (c *Condition) Truncate(s string, w int, tail string) string { + r := []rune(s) + tw := StringWidth(tail) + w -= tw + width := 0 + i := 0 + for ; i < len(r); i++ { + cw := RuneWidth(r[i]) + if width+cw > w { + break + } + width += cw + } + if i == len(r) { + return string(r[0:i]) + } + return string(r[0:i]) + tail +} + +// RuneWidth returns the number of cells in r. +// See http://www.unicode.org/reports/tr11/ +func RuneWidth(r rune) int { + return DefaultCondition.RuneWidth(r) +} + +// IsAmbiguousWidth returns whether is ambiguous width or not. +func IsAmbiguousWidth(r rune) bool { + for _, iv := range ambiguous { + if iv.first <= r && r <= iv.last { + return true + } + } + return false +} + +func StringWidth(s string) (width int) { + return DefaultCondition.StringWidth(s) +} + +func Truncate(s string, w int, tail string) string { + return DefaultCondition.Truncate(s, w, tail) +} diff --git a/Godeps/_workspace/src/github.com/mattn/go-runewidth/runewidth_js.go b/Godeps/_workspace/src/github.com/mattn/go-runewidth/runewidth_js.go new file mode 100644 index 0000000..0ce32c5 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mattn/go-runewidth/runewidth_js.go @@ -0,0 +1,8 @@ +// +build js + +package runewidth + +func IsEastAsian() bool { + // TODO: Implement this for the web. Detect east asian in a compatible way, and return true. + return false +} diff --git a/Godeps/_workspace/src/github.com/mattn/go-runewidth/runewidth_posix.go b/Godeps/_workspace/src/github.com/mattn/go-runewidth/runewidth_posix.go new file mode 100644 index 0000000..a449590 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mattn/go-runewidth/runewidth_posix.go @@ -0,0 +1,69 @@ +// +build !windows,!js + +package runewidth + +import ( + "os" + "regexp" + "strings" +) + +var reLoc = regexp.MustCompile(`^[a-z][a-z][a-z]?(?:_[A-Z][A-Z])?\.(.+)`) + +func IsEastAsian() bool { + locale := os.Getenv("LC_CTYPE") + if locale == "" { + locale = os.Getenv("LANG") + } + + // ignore C locale + if locale == "POSIX" || locale == "C" { + return false + } + if len(locale) > 1 && locale[0] == 'C' && (locale[1] == '.' || locale[1] == '-') { + return false + } + + charset := strings.ToLower(locale) + r := reLoc.FindStringSubmatch(locale) + if len(r) == 2 { + charset = strings.ToLower(r[1]) + } + + if strings.HasSuffix(charset, "@cjk_narrow") { + return false + } + + for pos, b := range []byte(charset) { + if b == '@' { + charset = charset[:pos] + break + } + } + + mbc_max := 1 + switch charset { + case "utf-8", "utf8": + mbc_max = 6 + case "jis": + mbc_max = 8 + case "eucjp": + mbc_max = 3 + case "euckr", "euccn": + mbc_max = 2 + case "sjis", "cp932", "cp51932", "cp936", "cp949", "cp950": + mbc_max = 2 + case "big5": + mbc_max = 2 + case "gbk", "gb2312": + mbc_max = 2 + } + + if mbc_max > 1 && (charset[0] != 'u' || + strings.HasPrefix(locale, "ja") || + strings.HasPrefix(locale, "ko") || + strings.HasPrefix(locale, "zh")) { + return true + } + return false +} diff --git a/Godeps/_workspace/src/github.com/mattn/go-runewidth/runewidth_test.go b/Godeps/_workspace/src/github.com/mattn/go-runewidth/runewidth_test.go new file mode 100644 index 0000000..fe86e10 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mattn/go-runewidth/runewidth_test.go @@ -0,0 +1,113 @@ +package runewidth + +import ( + "testing" +) + +var runewidthtests = []struct { + in rune + out int +}{ + {'世', 2}, + {'界', 2}, + {'セ', 1}, + {'カ', 1}, + {'イ', 1}, + {'☆', 2}, // double width in ambiguous + {'\x00', 0}, + {'\x01', 1}, + {'\u0300', 0}, +} + +func TestRuneWidth(t *testing.T) { + c := NewCondition() + c.EastAsianWidth = true + for _, tt := range runewidthtests { + if out := c.RuneWidth(tt.in); out != tt.out { + t.Errorf("Width(%q) = %v, want %v", tt.in, out, tt.out) + } + } +} + +var isambiguouswidthtests = []struct { + in rune + out bool +}{ + {'世', false}, + {'■', true}, + {'界', false}, + {'○', true}, + {'㈱', false}, + {'①', true}, + {'②', true}, + {'③', true}, + {'④', true}, + {'⑤', true}, + {'⑥', true}, + {'⑦', true}, + {'⑧', true}, + {'⑨', true}, + {'⑩', true}, + {'⑪', true}, + {'⑫', true}, + {'⑬', true}, + {'⑭', true}, + {'⑮', true}, + {'⑯', true}, + {'⑰', true}, + {'⑱', true}, + {'⑲', true}, + {'⑳', true}, + {'☆', true}, +} + +func TestIsAmbiguousWidth(t *testing.T) { + for _, tt := range isambiguouswidthtests { + if out := IsAmbiguousWidth(tt.in); out != tt.out { + t.Errorf("IsAmbiguousWidth(%q) = %v, want %v", tt.in, out, tt.out) + } + } +} + +var stringwidthtests = []struct { + in string + out int +}{ + {"■㈱の世界①", 12}, + {"スター☆", 8}, +} + +func TestStringWidth(t *testing.T) { + c := NewCondition() + c.EastAsianWidth = true + for _, tt := range stringwidthtests { + if out := c.StringWidth(tt.in); out != tt.out { + t.Errorf("StringWidth(%q) = %v, want %v", tt.in, out, tt.out) + } + } +} + +func TestStringWidthInvalid(t *testing.T) { + s := "こんにちわ\x00世界" + if out := StringWidth(s); out != 14 { + t.Errorf("StringWidth(%q) = %v, want %v", s, out, 14) + } +} + +func TestTruncate(t *testing.T) { + s := "あいうえおあいうえおえおおおおおおおおおおおおおおおおおおおおおおおおおおおおおお" + expected := "あいうえおあいうえおえおおおおおおおおおおおおおおおおおおおおおおおおおおお..." + + if out := Truncate(s, 80, "..."); out != expected { + t.Errorf("Truncate(%q) = %v, want %v", s, out, expected) + } +} + +func TestTruncateNoNeeded(t *testing.T) { + s := "あいうえおあい" + expected := "あいうえおあい" + + if out := Truncate(s, 80, "..."); out != expected { + t.Errorf("Truncate(%q) = %v, want %v", s, out, expected) + } +} diff --git a/Godeps/_workspace/src/github.com/mattn/go-runewidth/runewidth_windows.go b/Godeps/_workspace/src/github.com/mattn/go-runewidth/runewidth_windows.go new file mode 100644 index 0000000..bdd8445 --- /dev/null +++ b/Godeps/_workspace/src/github.com/mattn/go-runewidth/runewidth_windows.go @@ -0,0 +1,24 @@ +package runewidth + +import ( + "syscall" +) + +var ( + kernel32 = syscall.NewLazyDLL("kernel32") + procGetConsoleOutputCP = kernel32.NewProc("GetConsoleOutputCP") +) + +func IsEastAsian() bool { + r1, _, _ := procGetConsoleOutputCP.Call() + if r1 == 0 { + return false + } + + switch int(r1) { + case 932, 51932, 936, 949, 950: + return true + } + + return false +} diff --git a/Godeps/_workspace/src/github.com/nsf/termbox-go/AUTHORS b/Godeps/_workspace/src/github.com/nsf/termbox-go/AUTHORS new file mode 100644 index 0000000..fe26fb0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/nsf/termbox-go/AUTHORS @@ -0,0 +1,4 @@ +# Please keep this file sorted. + +Georg Reinke +nsf diff --git a/Godeps/_workspace/src/github.com/nsf/termbox-go/LICENSE b/Godeps/_workspace/src/github.com/nsf/termbox-go/LICENSE new file mode 100644 index 0000000..d9bc068 --- /dev/null +++ b/Godeps/_workspace/src/github.com/nsf/termbox-go/LICENSE @@ -0,0 +1,19 @@ +Copyright (C) 2012 termbox-go authors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/Godeps/_workspace/src/github.com/nsf/termbox-go/README.md b/Godeps/_workspace/src/github.com/nsf/termbox-go/README.md new file mode 100644 index 0000000..73ffb14 --- /dev/null +++ b/Godeps/_workspace/src/github.com/nsf/termbox-go/README.md @@ -0,0 +1,17 @@ +## Termbox +Termbox is a library that provides a minimalistic API which allows the programmer to write text-based user interfaces. The library is crossplatform and has both terminal-based implementations on *nix operating systems and a winapi console based implementation for windows operating systems. The basic idea is an abstraction of the greatest common subset of features available on all major terminals and other terminal-like APIs in a minimalistic fashion. Small API means it is easy to implement, test, maintain and learn it, that's what makes the termbox a distinct library in its area. + +### Installation +Install and update this go package with `go get -u github.com/nsf/termbox-go` + +### Examples +For examples of what can be done take a look at demos in the _demos directory. You can try them with go run: `go run _demos/keyboard.go` + +There are also some interesting projects using termbox-go: + - [godit](https://github.com/nsf/godit) is an emacsish lightweight text editor written using termbox. + - [gomatrix](https://github.com/GeertJohan/gomatrix) connects to The Matrix and displays it's data streams in your terminal. + - [httopd](https://github.com/verdverm/httopd) is top for httpd logs. + - [termui](https://github.com/gizak/termui) is a terminal dashboard. + +### API reference +[godoc.org/github.com/nsf/termbox-go](http://godoc.org/github.com/nsf/termbox-go) diff --git a/Godeps/_workspace/src/github.com/nsf/termbox-go/api.go b/Godeps/_workspace/src/github.com/nsf/termbox-go/api.go new file mode 100644 index 0000000..b08bca6 --- /dev/null +++ b/Godeps/_workspace/src/github.com/nsf/termbox-go/api.go @@ -0,0 +1,451 @@ +// +build !windows + +package termbox + +import "github.com/mattn/go-runewidth" +import "fmt" +import "os" +import "os/signal" +import "syscall" +import "runtime" + +// public API + +// Initializes termbox library. This function should be called before any other functions. +// After successful initialization, the library must be finalized using 'Close' function. +// +// Example usage: +// err := termbox.Init() +// if err != nil { +// panic(err) +// } +// defer termbox.Close() +func Init() error { + var err error + + out, err = os.OpenFile("/dev/tty", syscall.O_WRONLY, 0) + if err != nil { + return err + } + in, err = syscall.Open("/dev/tty", syscall.O_RDONLY, 0) + if err != nil { + return err + } + + err = setup_term() + if err != nil { + return fmt.Errorf("termbox: error while reading terminfo data: %v", err) + } + + signal.Notify(sigwinch, syscall.SIGWINCH) + signal.Notify(sigio, syscall.SIGIO) + + _, err = fcntl(in, syscall.F_SETFL, syscall.O_ASYNC|syscall.O_NONBLOCK) + if err != nil { + return err + } + _, err = fcntl(in, syscall.F_SETOWN, syscall.Getpid()) + if runtime.GOOS != "darwin" && err != nil { + return err + } + err = tcgetattr(out.Fd(), &orig_tios) + if err != nil { + return err + } + + tios := orig_tios + tios.Iflag &^= syscall_IGNBRK | syscall_BRKINT | syscall_PARMRK | + syscall_ISTRIP | syscall_INLCR | syscall_IGNCR | + syscall_ICRNL | syscall_IXON + tios.Oflag &^= syscall_OPOST + tios.Lflag &^= syscall_ECHO | syscall_ECHONL | syscall_ICANON | + syscall_ISIG | syscall_IEXTEN + tios.Cflag &^= syscall_CSIZE | syscall_PARENB + tios.Cflag |= syscall_CS8 + tios.Cc[syscall_VMIN] = 1 + tios.Cc[syscall_VTIME] = 0 + + err = tcsetattr(out.Fd(), &tios) + if err != nil { + return err + } + + out.WriteString(funcs[t_enter_ca]) + out.WriteString(funcs[t_enter_keypad]) + out.WriteString(funcs[t_hide_cursor]) + out.WriteString(funcs[t_clear_screen]) + + termw, termh = get_term_size(out.Fd()) + back_buffer.init(termw, termh) + front_buffer.init(termw, termh) + back_buffer.clear() + front_buffer.clear() + + go func() { + buf := make([]byte, 128) + for { + select { + case <-sigio: + for { + n, err := syscall.Read(in, buf) + if err == syscall.EAGAIN || err == syscall.EWOULDBLOCK { + break + } + select { + case input_comm <- input_event{buf[:n], err}: + ie := <-input_comm + buf = ie.data[:128] + case <-quit: + return + } + } + case <-quit: + return + } + } + }() + + IsInit = true + return nil +} + +// Interrupt an in-progress call to PollEvent by causing it to return +// EventInterrupt. Note that this function will block until the PollEvent +// function has successfully been interrupted. +func Interrupt() { + interrupt_comm <- struct{}{} +} + +// Finalizes termbox library, should be called after successful initialization +// when termbox's functionality isn't required anymore. +func Close() { + quit <- 1 + out.WriteString(funcs[t_show_cursor]) + out.WriteString(funcs[t_sgr0]) + out.WriteString(funcs[t_clear_screen]) + out.WriteString(funcs[t_exit_ca]) + out.WriteString(funcs[t_exit_keypad]) + out.WriteString(funcs[t_exit_mouse]) + tcsetattr(out.Fd(), &orig_tios) + + out.Close() + syscall.Close(in) + + // reset the state, so that on next Init() it will work again + termw = 0 + termh = 0 + input_mode = InputEsc + out = nil + in = 0 + lastfg = attr_invalid + lastbg = attr_invalid + lastx = coord_invalid + lasty = coord_invalid + cursor_x = cursor_hidden + cursor_y = cursor_hidden + foreground = ColorDefault + background = ColorDefault + IsInit = false +} + +// Synchronizes the internal back buffer with the terminal. +func Flush() error { + // invalidate cursor position + lastx = coord_invalid + lasty = coord_invalid + + update_size_maybe() + + for y := 0; y < front_buffer.height; y++ { + line_offset := y * front_buffer.width + for x := 0; x < front_buffer.width; { + cell_offset := line_offset + x + back := &back_buffer.cells[cell_offset] + front := &front_buffer.cells[cell_offset] + if back.Ch < ' ' { + back.Ch = ' ' + } + w := runewidth.RuneWidth(back.Ch) + if w == 0 || w == 2 && runewidth.IsAmbiguousWidth(back.Ch) { + w = 1 + } + if *back == *front { + x += w + continue + } + *front = *back + send_attr(back.Fg, back.Bg) + + if w == 2 && x == front_buffer.width-1 { + // there's not enough space for 2-cells rune, + // let's just put a space in there + send_char(x, y, ' ') + } else { + send_char(x, y, back.Ch) + if w == 2 { + next := cell_offset + 1 + front_buffer.cells[next] = Cell{ + Ch: 0, + Fg: back.Fg, + Bg: back.Bg, + } + } + } + x += w + } + } + if !is_cursor_hidden(cursor_x, cursor_y) { + write_cursor(cursor_x, cursor_y) + } + return flush() +} + +// Sets the position of the cursor. See also HideCursor(). +func SetCursor(x, y int) { + if is_cursor_hidden(cursor_x, cursor_y) && !is_cursor_hidden(x, y) { + outbuf.WriteString(funcs[t_show_cursor]) + } + + if !is_cursor_hidden(cursor_x, cursor_y) && is_cursor_hidden(x, y) { + outbuf.WriteString(funcs[t_hide_cursor]) + } + + cursor_x, cursor_y = x, y + if !is_cursor_hidden(cursor_x, cursor_y) { + write_cursor(cursor_x, cursor_y) + } +} + +// The shortcut for SetCursor(-1, -1). +func HideCursor() { + SetCursor(cursor_hidden, cursor_hidden) +} + +// Changes cell's parameters in the internal back buffer at the specified +// position. +func SetCell(x, y int, ch rune, fg, bg Attribute) { + if x < 0 || x >= back_buffer.width { + return + } + if y < 0 || y >= back_buffer.height { + return + } + + back_buffer.cells[y*back_buffer.width+x] = Cell{ch, fg, bg} +} + +// Returns a slice into the termbox's back buffer. You can get its dimensions +// using 'Size' function. The slice remains valid as long as no 'Clear' or +// 'Flush' function calls were made after call to this function. +func CellBuffer() []Cell { + return back_buffer.cells +} + +// After getting a raw event from PollRawEvent function call, you can parse it +// again into an ordinary one using termbox logic. That is parse an event as +// termbox would do it. Returned event in addition to usual Event struct fields +// sets N field to the amount of bytes used within 'data' slice. If the length +// of 'data' slice is zero or event cannot be parsed for some other reason, the +// function will return a special event type: EventNone. +// +// IMPORTANT: EventNone may contain a non-zero N, which means you should skip +// these bytes, because termbox cannot recognize them. +// +// NOTE: This API is experimental and may change in future. +func ParseEvent(data []byte) Event { + event := Event{Type: EventKey} + ok := extract_event(data, &event) + if !ok { + return Event{Type: EventNone, N: event.N} + } + return event +} + +// Wait for an event and return it. This is a blocking function call. Instead +// of EventKey and EventMouse it returns EventRaw events. Raw event is written +// into `data` slice and Event's N field is set to the amount of bytes written. +// The minimum required length of the 'data' slice is 1. This requirement may +// vary on different platforms. +// +// NOTE: This API is experimental and may change in future. +func PollRawEvent(data []byte) Event { + if len(data) == 0 { + panic("len(data) >= 1 is a requirement") + } + + var event Event + if extract_raw_event(data, &event) { + return event + } + + for { + select { + case ev := <-input_comm: + if ev.err != nil { + return Event{Type: EventError, Err: ev.err} + } + + inbuf = append(inbuf, ev.data...) + input_comm <- ev + if extract_raw_event(data, &event) { + return event + } + case <-interrupt_comm: + event.Type = EventInterrupt + return event + + case <-sigwinch: + event.Type = EventResize + event.Width, event.Height = get_term_size(out.Fd()) + return event + } + } +} + +// Wait for an event and return it. This is a blocking function call. +func PollEvent() Event { + var event Event + + // try to extract event from input buffer, return on success + event.Type = EventKey + ok := extract_event(inbuf, &event) + if event.N != 0 { + copy(inbuf, inbuf[event.N:]) + inbuf = inbuf[:len(inbuf)-event.N] + } + if ok { + return event + } + + for { + select { + case ev := <-input_comm: + if ev.err != nil { + return Event{Type: EventError, Err: ev.err} + } + + inbuf = append(inbuf, ev.data...) + input_comm <- ev + ok := extract_event(inbuf, &event) + if event.N != 0 { + copy(inbuf, inbuf[event.N:]) + inbuf = inbuf[:len(inbuf)-event.N] + } + if ok { + return event + } + case <-interrupt_comm: + event.Type = EventInterrupt + return event + + case <-sigwinch: + event.Type = EventResize + event.Width, event.Height = get_term_size(out.Fd()) + return event + } + } + panic("unreachable") +} + +// Returns the size of the internal back buffer (which is mostly the same as +// terminal's window size in characters). But it doesn't always match the size +// of the terminal window, after the terminal size has changed, the internal +// back buffer will get in sync only after Clear or Flush function calls. +func Size() (int, int) { + return termw, termh +} + +// Clears the internal back buffer. +func Clear(fg, bg Attribute) error { + foreground, background = fg, bg + err := update_size_maybe() + back_buffer.clear() + return err +} + +// Sets termbox input mode. Termbox has two input modes: +// +// 1. Esc input mode. When ESC sequence is in the buffer and it doesn't match +// any known sequence. ESC means KeyEsc. This is the default input mode. +// +// 2. Alt input mode. When ESC sequence is in the buffer and it doesn't match +// any known sequence. ESC enables ModAlt modifier for the next keyboard event. +// +// Both input modes can be OR'ed with Mouse mode. Setting Mouse mode bit up will +// enable mouse button click events. +// +// If 'mode' is InputCurrent, returns the current input mode. See also Input* +// constants. +func SetInputMode(mode InputMode) InputMode { + if mode == InputCurrent { + return input_mode + } + if mode&InputMouse != 0 { + out.WriteString(funcs[t_enter_mouse]) + } else { + out.WriteString(funcs[t_exit_mouse]) + } + + input_mode = mode + return input_mode +} + +// Sets the termbox output mode. Termbox has four output options: +// 1. OutputNormal => [1..8] +// This mode provides 8 different colors: +// black, red, green, yellow, blue, magenta, cyan, white +// Shortcut: ColorBlack, ColorRed, ... +// Attributes: AttrBold, AttrUnderline, AttrReverse +// +// Example usage: +// SetCell(x, y, '@', ColorBlack | AttrBold, ColorRed); +// +// 2. Output256 => [1..256] +// In this mode you can leverage the 256 terminal mode: +// 0x00 - 0x07: the 8 colors as in OutputNormal +// 0x08 - 0x0f: Color* | AttrBold +// 0x10 - 0xe7: 216 different colors +// 0xe8 - 0xff: 24 different shades of grey +// +// Example usage: +// SetCell(x, y, '@', 184, 240); +// SetCell(x, y, '@', 0xb8, 0xf0); +// +// 3. Output216 => [1..216] +// This mode supports the 3rd range of the 256 mode only. +// But you dont need to provide an offset. +// +// 4. OutputGrayscale => [1..24] +// This mode supports the 4th range of the 256 mode only. +// But you dont need to provide an offset. +// +// In all modes, 0 represents the default color. +// +// `go run _demos/output.go` to see its impact on your terminal. +// +// If 'mode' is OutputCurrent, it returns the current output mode. +// +// Note that this may return a different OutputMode than the one requested, +// as the requested mode may not be available on the target platform. +func SetOutputMode(mode OutputMode) OutputMode { + if mode == OutputCurrent { + return output_mode + } + + output_mode = mode + return output_mode +} + +// Sync comes handy when something causes desync between termbox's understanding +// of a terminal buffer and the reality. Such as a third party process. Sync +// forces a complete resync between the termbox and a terminal, it may not be +// visually pretty though. +func Sync() error { + front_buffer.clear() + err := send_clear() + if err != nil { + return err + } + + return Flush() +} diff --git a/Godeps/_workspace/src/github.com/nsf/termbox-go/api_common.go b/Godeps/_workspace/src/github.com/nsf/termbox-go/api_common.go new file mode 100644 index 0000000..c0069fb --- /dev/null +++ b/Godeps/_workspace/src/github.com/nsf/termbox-go/api_common.go @@ -0,0 +1,183 @@ +// termbox is a library for creating cross-platform text-based interfaces +package termbox + +// public API, common OS agnostic part + +type ( + InputMode int + OutputMode int + EventType uint8 + Modifier uint8 + Key uint16 + Attribute uint16 +) + +// This type represents a termbox event. The 'Mod', 'Key' and 'Ch' fields are +// valid if 'Type' is EventKey. The 'Width' and 'Height' fields are valid if +// 'Type' is EventResize. The 'Err' field is valid if 'Type' is EventError. +type Event struct { + Type EventType // one of Event* constants + Mod Modifier // one of Mod* constants or 0 + Key Key // one of Key* constants, invalid if 'Ch' is not 0 + Ch rune // a unicode character + Width int // width of the screen + Height int // height of the screen + Err error // error in case if input failed + MouseX int // x coord of mouse + MouseY int // y coord of mouse + N int // number of bytes written when getting a raw event +} + +// A cell, single conceptual entity on the screen. The screen is basically a 2d +// array of cells. 'Ch' is a unicode character, 'Fg' and 'Bg' are foreground +// and background attributes respectively. +type Cell struct { + Ch rune + Fg Attribute + Bg Attribute +} + +// To know if termbox has been initialized or not +var ( + IsInit bool = false +) + +// Key constants, see Event.Key field. +const ( + KeyF1 Key = 0xFFFF - iota + KeyF2 + KeyF3 + KeyF4 + KeyF5 + KeyF6 + KeyF7 + KeyF8 + KeyF9 + KeyF10 + KeyF11 + KeyF12 + KeyInsert + KeyDelete + KeyHome + KeyEnd + KeyPgup + KeyPgdn + KeyArrowUp + KeyArrowDown + KeyArrowLeft + KeyArrowRight + key_min // see terminfo + MouseLeft + MouseMiddle + MouseRight +) + +const ( + KeyCtrlTilde Key = 0x00 + KeyCtrl2 Key = 0x00 + KeyCtrlSpace Key = 0x00 + KeyCtrlA Key = 0x01 + KeyCtrlB Key = 0x02 + KeyCtrlC Key = 0x03 + KeyCtrlD Key = 0x04 + KeyCtrlE Key = 0x05 + KeyCtrlF Key = 0x06 + KeyCtrlG Key = 0x07 + KeyBackspace Key = 0x08 + KeyCtrlH Key = 0x08 + KeyTab Key = 0x09 + KeyCtrlI Key = 0x09 + KeyCtrlJ Key = 0x0A + KeyCtrlK Key = 0x0B + KeyCtrlL Key = 0x0C + KeyEnter Key = 0x0D + KeyCtrlM Key = 0x0D + KeyCtrlN Key = 0x0E + KeyCtrlO Key = 0x0F + KeyCtrlP Key = 0x10 + KeyCtrlQ Key = 0x11 + KeyCtrlR Key = 0x12 + KeyCtrlS Key = 0x13 + KeyCtrlT Key = 0x14 + KeyCtrlU Key = 0x15 + KeyCtrlV Key = 0x16 + KeyCtrlW Key = 0x17 + KeyCtrlX Key = 0x18 + KeyCtrlY Key = 0x19 + KeyCtrlZ Key = 0x1A + KeyEsc Key = 0x1B + KeyCtrlLsqBracket Key = 0x1B + KeyCtrl3 Key = 0x1B + KeyCtrl4 Key = 0x1C + KeyCtrlBackslash Key = 0x1C + KeyCtrl5 Key = 0x1D + KeyCtrlRsqBracket Key = 0x1D + KeyCtrl6 Key = 0x1E + KeyCtrl7 Key = 0x1F + KeyCtrlSlash Key = 0x1F + KeyCtrlUnderscore Key = 0x1F + KeySpace Key = 0x20 + KeyBackspace2 Key = 0x7F + KeyCtrl8 Key = 0x7F +) + +// Alt modifier constant, see Event.Mod field and SetInputMode function. +const ( + ModAlt Modifier = 0x01 +) + +// Cell colors, you can combine a color with multiple attributes using bitwise +// OR ('|'). +const ( + ColorDefault Attribute = iota + ColorBlack + ColorRed + ColorGreen + ColorYellow + ColorBlue + ColorMagenta + ColorCyan + ColorWhite +) + +// Cell attributes, it is possible to use multiple attributes by combining them +// using bitwise OR ('|'). Although, colors cannot be combined. But you can +// combine attributes and a single color. +// +// It's worth mentioning that some platforms don't support certain attibutes. +// For example windows console doesn't support AttrUnderline. And on some +// terminals applying AttrBold to background may result in blinking text. Use +// them with caution and test your code on various terminals. +const ( + AttrBold Attribute = 1 << (iota + 9) + AttrUnderline + AttrReverse +) + +// Input mode. See SetInputMode function. +const ( + InputEsc InputMode = 1 << iota + InputAlt + InputMouse + InputCurrent InputMode = 0 +) + +// Output mode. See SetOutputMode function. +const ( + OutputCurrent OutputMode = iota + OutputNormal + Output256 + Output216 + OutputGrayscale +) + +// Event type. See Event.Type field. +const ( + EventKey EventType = iota + EventResize + EventMouse + EventError + EventInterrupt + EventRaw + EventNone +) diff --git a/Godeps/_workspace/src/github.com/nsf/termbox-go/api_windows.go b/Godeps/_workspace/src/github.com/nsf/termbox-go/api_windows.go new file mode 100644 index 0000000..78d954b --- /dev/null +++ b/Godeps/_workspace/src/github.com/nsf/termbox-go/api_windows.go @@ -0,0 +1,235 @@ +package termbox + +import ( + "syscall" +) + +// public API + +// Initializes termbox library. This function should be called before any other functions. +// After successful initialization, the library must be finalized using 'Close' function. +// +// Example usage: +// err := termbox.Init() +// if err != nil { +// panic(err) +// } +// defer termbox.Close() +func Init() error { + var err error + + interrupt, err = create_event() + if err != nil { + return err + } + + in, err = syscall.Open("CONIN$", syscall.O_RDWR, 0) + if err != nil { + return err + } + out, err = syscall.Open("CONOUT$", syscall.O_RDWR, 0) + if err != nil { + return err + } + + err = get_console_mode(in, &orig_mode) + if err != nil { + return err + } + + err = set_console_mode(in, enable_window_input) + if err != nil { + return err + } + + orig_size = get_term_size(out) + win_size := get_win_size(out) + + err = set_console_screen_buffer_size(out, win_size) + if err != nil { + return err + } + + err = get_console_cursor_info(out, &orig_cursor_info) + if err != nil { + return err + } + + show_cursor(false) + term_size = get_term_size(out) + back_buffer.init(int(term_size.x), int(term_size.y)) + front_buffer.init(int(term_size.x), int(term_size.y)) + back_buffer.clear() + front_buffer.clear() + clear() + + diffbuf = make([]diff_msg, 0, 32) + + go input_event_producer() + IsInit = true + return nil +} + +// Finalizes termbox library, should be called after successful initialization +// when termbox's functionality isn't required anymore. +func Close() { + // we ignore errors here, because we can't really do anything about them + Clear(0, 0) + Flush() + + // stop event producer + cancel_comm <- true + set_event(interrupt) + <-cancel_done_comm + + set_console_cursor_info(out, &orig_cursor_info) + set_console_cursor_position(out, coord{}) + set_console_screen_buffer_size(out, orig_size) + set_console_mode(in, orig_mode) + syscall.Close(in) + syscall.Close(out) + syscall.Close(interrupt) + IsInit = false +} + +// Interrupt an in-progress call to PollEvent by causing it to return +// EventInterrupt. Note that this function will block until the PollEvent +// function has successfully been interrupted. +func Interrupt() { + interrupt_comm <- struct{}{} +} + +// Synchronizes the internal back buffer with the terminal. +func Flush() error { + update_size_maybe() + prepare_diff_messages() + for _, diff := range diffbuf { + r := small_rect{ + left: 0, + top: diff.pos, + right: term_size.x - 1, + bottom: diff.pos + diff.lines - 1, + } + write_console_output(out, diff.chars, r) + } + if !is_cursor_hidden(cursor_x, cursor_y) { + move_cursor(cursor_x, cursor_y) + } + return nil +} + +// Sets the position of the cursor. See also HideCursor(). +func SetCursor(x, y int) { + if is_cursor_hidden(cursor_x, cursor_y) && !is_cursor_hidden(x, y) { + show_cursor(true) + } + + if !is_cursor_hidden(cursor_x, cursor_y) && is_cursor_hidden(x, y) { + show_cursor(false) + } + + cursor_x, cursor_y = x, y + if !is_cursor_hidden(cursor_x, cursor_y) { + move_cursor(cursor_x, cursor_y) + } +} + +// The shortcut for SetCursor(-1, -1). +func HideCursor() { + SetCursor(cursor_hidden, cursor_hidden) +} + +// Changes cell's parameters in the internal back buffer at the specified +// position. +func SetCell(x, y int, ch rune, fg, bg Attribute) { + if x < 0 || x >= back_buffer.width { + return + } + if y < 0 || y >= back_buffer.height { + return + } + + back_buffer.cells[y*back_buffer.width+x] = Cell{ch, fg, bg} +} + +// Returns a slice into the termbox's back buffer. You can get its dimensions +// using 'Size' function. The slice remains valid as long as no 'Clear' or +// 'Flush' function calls were made after call to this function. +func CellBuffer() []Cell { + return back_buffer.cells +} + +// Wait for an event and return it. This is a blocking function call. +func PollEvent() Event { + select { + case ev := <-input_comm: + return ev + case <-interrupt_comm: + return Event{Type: EventInterrupt} + } +} + +// Returns the size of the internal back buffer (which is mostly the same as +// console's window size in characters). But it doesn't always match the size +// of the console window, after the console size has changed, the internal back +// buffer will get in sync only after Clear or Flush function calls. +func Size() (int, int) { + return int(term_size.x), int(term_size.y) +} + +// Clears the internal back buffer. +func Clear(fg, bg Attribute) error { + foreground, background = fg, bg + update_size_maybe() + back_buffer.clear() + return nil +} + +// Sets termbox input mode. Termbox has two input modes: +// +// 1. Esc input mode. When ESC sequence is in the buffer and it doesn't match +// any known sequence. ESC means KeyEsc. This is the default input mode. +// +// 2. Alt input mode. When ESC sequence is in the buffer and it doesn't match +// any known sequence. ESC enables ModAlt modifier for the next keyboard event. +// +// Both input modes can be OR'ed with Mouse mode. Setting Mouse mode bit up will +// enable mouse button click events. +// +// If 'mode' is InputCurrent, returns the current input mode. See also Input* +// constants. +func SetInputMode(mode InputMode) InputMode { + if mode == InputCurrent { + return input_mode + } + if mode&InputMouse != 0 { + err := set_console_mode(in, enable_window_input|enable_mouse_input|enable_extended_flags) + if err != nil { + panic(err) + } + } else { + err := set_console_mode(in, enable_window_input) + if err != nil { + panic(err) + } + } + + input_mode = mode + return input_mode +} + +// Sets the termbox output mode. +// +// Windows console does not support extra colour modes, +// so this will always set and return OutputNormal. +func SetOutputMode(mode OutputMode) OutputMode { + return OutputNormal +} + +// Sync comes handy when something causes desync between termbox's understanding +// of a terminal buffer and the reality. Such as a third party process. Sync +// forces a complete resync between the termbox and a terminal, it may not be +// visually pretty though. At the moment on Windows it does nothing. +func Sync() error { + return nil +} diff --git a/Godeps/_workspace/src/github.com/nsf/termbox-go/collect_terminfo.py b/Godeps/_workspace/src/github.com/nsf/termbox-go/collect_terminfo.py new file mode 100644 index 0000000..5e50975 --- /dev/null +++ b/Godeps/_workspace/src/github.com/nsf/termbox-go/collect_terminfo.py @@ -0,0 +1,110 @@ +#!/usr/bin/env python + +import sys, os, subprocess + +def escaped(s): + return repr(s)[1:-1] + +def tput(term, name): + try: + return subprocess.check_output(['tput', '-T%s' % term, name]).decode() + except subprocess.CalledProcessError as e: + return e.output.decode() + + +def w(s): + if s == None: + return + sys.stdout.write(s) + +terminals = { + 'xterm' : 'xterm', + 'rxvt-256color' : 'rxvt_256color', + 'rxvt-unicode' : 'rxvt_unicode', + 'linux' : 'linux', + 'Eterm' : 'eterm', + 'screen' : 'screen' +} + +keys = [ + "F1", "kf1", + "F2", "kf2", + "F3", "kf3", + "F4", "kf4", + "F5", "kf5", + "F6", "kf6", + "F7", "kf7", + "F8", "kf8", + "F9", "kf9", + "F10", "kf10", + "F11", "kf11", + "F12", "kf12", + "INSERT", "kich1", + "DELETE", "kdch1", + "HOME", "khome", + "END", "kend", + "PGUP", "kpp", + "PGDN", "knp", + "KEY_UP", "kcuu1", + "KEY_DOWN", "kcud1", + "KEY_LEFT", "kcub1", + "KEY_RIGHT", "kcuf1" +] + +funcs = [ + "T_ENTER_CA", "smcup", + "T_EXIT_CA", "rmcup", + "T_SHOW_CURSOR", "cnorm", + "T_HIDE_CURSOR", "civis", + "T_CLEAR_SCREEN", "clear", + "T_SGR0", "sgr0", + "T_UNDERLINE", "smul", + "T_BOLD", "bold", + "T_BLINK", "blink", + "T_REVERSE", "rev", + "T_ENTER_KEYPAD", "smkx", + "T_EXIT_KEYPAD", "rmkx" +] + +def iter_pairs(iterable): + iterable = iter(iterable) + while True: + yield (next(iterable), next(iterable)) + +def do_term(term, nick): + w("// %s\n" % term) + w("var %s_keys = []string{\n\t" % nick) + for k, v in iter_pairs(keys): + w('"') + w(escaped(tput(term, v))) + w('",') + w("\n}\n") + w("var %s_funcs = []string{\n\t" % nick) + for k,v in iter_pairs(funcs): + w('"') + if v == "sgr": + w("\\033[3%d;4%dm") + elif v == "cup": + w("\\033[%d;%dH") + else: + w(escaped(tput(term, v))) + w('", ') + w("\n}\n\n") + +def do_terms(d): + w("var terms = []struct {\n") + w("\tname string\n") + w("\tkeys []string\n") + w("\tfuncs []string\n") + w("}{\n") + for k, v in d.items(): + w('\t{"%s", %s_keys, %s_funcs},\n' % (k, v, v)) + w("}\n\n") + +w("// +build !windows\n\npackage termbox\n\n") + +for k,v in terminals.items(): + do_term(k, v) + +do_terms(terminals) + diff --git a/Godeps/_workspace/src/github.com/nsf/termbox-go/syscalls.go b/Godeps/_workspace/src/github.com/nsf/termbox-go/syscalls.go new file mode 100644 index 0000000..4f52bb9 --- /dev/null +++ b/Godeps/_workspace/src/github.com/nsf/termbox-go/syscalls.go @@ -0,0 +1,39 @@ +// +build ignore + +package termbox + +/* +#include +#include +*/ +import "C" + +type syscall_Termios C.struct_termios + +const ( + syscall_IGNBRK = C.IGNBRK + syscall_BRKINT = C.BRKINT + syscall_PARMRK = C.PARMRK + syscall_ISTRIP = C.ISTRIP + syscall_INLCR = C.INLCR + syscall_IGNCR = C.IGNCR + syscall_ICRNL = C.ICRNL + syscall_IXON = C.IXON + syscall_OPOST = C.OPOST + syscall_ECHO = C.ECHO + syscall_ECHONL = C.ECHONL + syscall_ICANON = C.ICANON + syscall_ISIG = C.ISIG + syscall_IEXTEN = C.IEXTEN + syscall_CSIZE = C.CSIZE + syscall_PARENB = C.PARENB + syscall_CS8 = C.CS8 + syscall_VMIN = C.VMIN + syscall_VTIME = C.VTIME + + // on darwin change these to (on *bsd too?): + // C.TIOCGETA + // C.TIOCSETA + syscall_TCGETS = C.TCGETS + syscall_TCSETS = C.TCSETS +) diff --git a/Godeps/_workspace/src/github.com/nsf/termbox-go/syscalls_darwin_386.go b/Godeps/_workspace/src/github.com/nsf/termbox-go/syscalls_darwin_386.go new file mode 100644 index 0000000..e03624e --- /dev/null +++ b/Godeps/_workspace/src/github.com/nsf/termbox-go/syscalls_darwin_386.go @@ -0,0 +1,39 @@ +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs syscalls.go + +package termbox + +type syscall_Termios struct { + Iflag uint32 + Oflag uint32 + Cflag uint32 + Lflag uint32 + Cc [20]uint8 + Ispeed uint32 + Ospeed uint32 +} + +const ( + syscall_IGNBRK = 0x1 + syscall_BRKINT = 0x2 + syscall_PARMRK = 0x8 + syscall_ISTRIP = 0x20 + syscall_INLCR = 0x40 + syscall_IGNCR = 0x80 + syscall_ICRNL = 0x100 + syscall_IXON = 0x200 + syscall_OPOST = 0x1 + syscall_ECHO = 0x8 + syscall_ECHONL = 0x10 + syscall_ICANON = 0x100 + syscall_ISIG = 0x80 + syscall_IEXTEN = 0x400 + syscall_CSIZE = 0x300 + syscall_PARENB = 0x1000 + syscall_CS8 = 0x300 + syscall_VMIN = 0x10 + syscall_VTIME = 0x11 + + syscall_TCGETS = 0x402c7413 + syscall_TCSETS = 0x802c7414 +) diff --git a/Godeps/_workspace/src/github.com/nsf/termbox-go/syscalls_darwin_amd64.go b/Godeps/_workspace/src/github.com/nsf/termbox-go/syscalls_darwin_amd64.go new file mode 100644 index 0000000..11f25be --- /dev/null +++ b/Godeps/_workspace/src/github.com/nsf/termbox-go/syscalls_darwin_amd64.go @@ -0,0 +1,40 @@ +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs syscalls.go + +package termbox + +type syscall_Termios struct { + Iflag uint64 + Oflag uint64 + Cflag uint64 + Lflag uint64 + Cc [20]uint8 + Pad_cgo_0 [4]byte + Ispeed uint64 + Ospeed uint64 +} + +const ( + syscall_IGNBRK = 0x1 + syscall_BRKINT = 0x2 + syscall_PARMRK = 0x8 + syscall_ISTRIP = 0x20 + syscall_INLCR = 0x40 + syscall_IGNCR = 0x80 + syscall_ICRNL = 0x100 + syscall_IXON = 0x200 + syscall_OPOST = 0x1 + syscall_ECHO = 0x8 + syscall_ECHONL = 0x10 + syscall_ICANON = 0x100 + syscall_ISIG = 0x80 + syscall_IEXTEN = 0x400 + syscall_CSIZE = 0x300 + syscall_PARENB = 0x1000 + syscall_CS8 = 0x300 + syscall_VMIN = 0x10 + syscall_VTIME = 0x11 + + syscall_TCGETS = 0x40487413 + syscall_TCSETS = 0x80487414 +) diff --git a/Godeps/_workspace/src/github.com/nsf/termbox-go/syscalls_freebsd.go b/Godeps/_workspace/src/github.com/nsf/termbox-go/syscalls_freebsd.go new file mode 100644 index 0000000..e03624e --- /dev/null +++ b/Godeps/_workspace/src/github.com/nsf/termbox-go/syscalls_freebsd.go @@ -0,0 +1,39 @@ +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs syscalls.go + +package termbox + +type syscall_Termios struct { + Iflag uint32 + Oflag uint32 + Cflag uint32 + Lflag uint32 + Cc [20]uint8 + Ispeed uint32 + Ospeed uint32 +} + +const ( + syscall_IGNBRK = 0x1 + syscall_BRKINT = 0x2 + syscall_PARMRK = 0x8 + syscall_ISTRIP = 0x20 + syscall_INLCR = 0x40 + syscall_IGNCR = 0x80 + syscall_ICRNL = 0x100 + syscall_IXON = 0x200 + syscall_OPOST = 0x1 + syscall_ECHO = 0x8 + syscall_ECHONL = 0x10 + syscall_ICANON = 0x100 + syscall_ISIG = 0x80 + syscall_IEXTEN = 0x400 + syscall_CSIZE = 0x300 + syscall_PARENB = 0x1000 + syscall_CS8 = 0x300 + syscall_VMIN = 0x10 + syscall_VTIME = 0x11 + + syscall_TCGETS = 0x402c7413 + syscall_TCSETS = 0x802c7414 +) diff --git a/Godeps/_workspace/src/github.com/nsf/termbox-go/syscalls_linux.go b/Godeps/_workspace/src/github.com/nsf/termbox-go/syscalls_linux.go new file mode 100644 index 0000000..b88960d --- /dev/null +++ b/Godeps/_workspace/src/github.com/nsf/termbox-go/syscalls_linux.go @@ -0,0 +1,33 @@ +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs syscalls.go + +package termbox + +import "syscall" + +type syscall_Termios syscall.Termios + +const ( + syscall_IGNBRK = syscall.IGNBRK + syscall_BRKINT = syscall.BRKINT + syscall_PARMRK = syscall.PARMRK + syscall_ISTRIP = syscall.ISTRIP + syscall_INLCR = syscall.INLCR + syscall_IGNCR = syscall.IGNCR + syscall_ICRNL = syscall.ICRNL + syscall_IXON = syscall.IXON + syscall_OPOST = syscall.OPOST + syscall_ECHO = syscall.ECHO + syscall_ECHONL = syscall.ECHONL + syscall_ICANON = syscall.ICANON + syscall_ISIG = syscall.ISIG + syscall_IEXTEN = syscall.IEXTEN + syscall_CSIZE = syscall.CSIZE + syscall_PARENB = syscall.PARENB + syscall_CS8 = syscall.CS8 + syscall_VMIN = syscall.VMIN + syscall_VTIME = syscall.VTIME + + syscall_TCGETS = syscall.TCGETS + syscall_TCSETS = syscall.TCSETS +) diff --git a/Godeps/_workspace/src/github.com/nsf/termbox-go/syscalls_netbsd.go b/Godeps/_workspace/src/github.com/nsf/termbox-go/syscalls_netbsd.go new file mode 100644 index 0000000..49a3355 --- /dev/null +++ b/Godeps/_workspace/src/github.com/nsf/termbox-go/syscalls_netbsd.go @@ -0,0 +1,39 @@ +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs syscalls.go + +package termbox + +type syscall_Termios struct { + Iflag uint32 + Oflag uint32 + Cflag uint32 + Lflag uint32 + Cc [20]uint8 + Ispeed int32 + Ospeed int32 +} + +const ( + syscall_IGNBRK = 0x1 + syscall_BRKINT = 0x2 + syscall_PARMRK = 0x8 + syscall_ISTRIP = 0x20 + syscall_INLCR = 0x40 + syscall_IGNCR = 0x80 + syscall_ICRNL = 0x100 + syscall_IXON = 0x200 + syscall_OPOST = 0x1 + syscall_ECHO = 0x8 + syscall_ECHONL = 0x10 + syscall_ICANON = 0x100 + syscall_ISIG = 0x80 + syscall_IEXTEN = 0x400 + syscall_CSIZE = 0x300 + syscall_PARENB = 0x1000 + syscall_CS8 = 0x300 + syscall_VMIN = 0x10 + syscall_VTIME = 0x11 + + syscall_TCGETS = 0x402c7413 + syscall_TCSETS = 0x802c7414 +) diff --git a/Godeps/_workspace/src/github.com/nsf/termbox-go/syscalls_openbsd.go b/Godeps/_workspace/src/github.com/nsf/termbox-go/syscalls_openbsd.go new file mode 100644 index 0000000..49a3355 --- /dev/null +++ b/Godeps/_workspace/src/github.com/nsf/termbox-go/syscalls_openbsd.go @@ -0,0 +1,39 @@ +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs syscalls.go + +package termbox + +type syscall_Termios struct { + Iflag uint32 + Oflag uint32 + Cflag uint32 + Lflag uint32 + Cc [20]uint8 + Ispeed int32 + Ospeed int32 +} + +const ( + syscall_IGNBRK = 0x1 + syscall_BRKINT = 0x2 + syscall_PARMRK = 0x8 + syscall_ISTRIP = 0x20 + syscall_INLCR = 0x40 + syscall_IGNCR = 0x80 + syscall_ICRNL = 0x100 + syscall_IXON = 0x200 + syscall_OPOST = 0x1 + syscall_ECHO = 0x8 + syscall_ECHONL = 0x10 + syscall_ICANON = 0x100 + syscall_ISIG = 0x80 + syscall_IEXTEN = 0x400 + syscall_CSIZE = 0x300 + syscall_PARENB = 0x1000 + syscall_CS8 = 0x300 + syscall_VMIN = 0x10 + syscall_VTIME = 0x11 + + syscall_TCGETS = 0x402c7413 + syscall_TCSETS = 0x802c7414 +) diff --git a/Godeps/_workspace/src/github.com/nsf/termbox-go/syscalls_windows.go b/Godeps/_workspace/src/github.com/nsf/termbox-go/syscalls_windows.go new file mode 100644 index 0000000..472d002 --- /dev/null +++ b/Godeps/_workspace/src/github.com/nsf/termbox-go/syscalls_windows.go @@ -0,0 +1,61 @@ +// Created by cgo -godefs - DO NOT EDIT +// cgo -godefs -- -DUNICODE syscalls.go + +package termbox + +const ( + foreground_blue = 0x1 + foreground_green = 0x2 + foreground_red = 0x4 + foreground_intensity = 0x8 + background_blue = 0x10 + background_green = 0x20 + background_red = 0x40 + background_intensity = 0x80 + std_input_handle = -0xa + std_output_handle = -0xb + key_event = 0x1 + mouse_event = 0x2 + window_buffer_size_event = 0x4 + enable_window_input = 0x8 + enable_mouse_input = 0x10 + enable_extended_flags = 0x80 + + vk_f1 = 0x70 + vk_f2 = 0x71 + vk_f3 = 0x72 + vk_f4 = 0x73 + vk_f5 = 0x74 + vk_f6 = 0x75 + vk_f7 = 0x76 + vk_f8 = 0x77 + vk_f9 = 0x78 + vk_f10 = 0x79 + vk_f11 = 0x7a + vk_f12 = 0x7b + vk_insert = 0x2d + vk_delete = 0x2e + vk_home = 0x24 + vk_end = 0x23 + vk_pgup = 0x21 + vk_pgdn = 0x22 + vk_arrow_up = 0x26 + vk_arrow_down = 0x28 + vk_arrow_left = 0x25 + vk_arrow_right = 0x27 + vk_backspace = 0x8 + vk_tab = 0x9 + vk_enter = 0xd + vk_esc = 0x1b + vk_space = 0x20 + + left_alt_pressed = 0x2 + left_ctrl_pressed = 0x8 + right_alt_pressed = 0x1 + right_ctrl_pressed = 0x4 + shift_pressed = 0x10 + + generic_read = 0x80000000 + generic_write = 0x40000000 + console_textmode_buffer = 0x1 +) diff --git a/Godeps/_workspace/src/github.com/nsf/termbox-go/termbox.go b/Godeps/_workspace/src/github.com/nsf/termbox-go/termbox.go new file mode 100644 index 0000000..0aee8ac --- /dev/null +++ b/Godeps/_workspace/src/github.com/nsf/termbox-go/termbox.go @@ -0,0 +1,407 @@ +// +build !windows + +package termbox + +import "unicode/utf8" +import "bytes" +import "syscall" +import "unsafe" +import "strings" +import "strconv" +import "os" +import "io" + +// private API + +const ( + t_enter_ca = iota + t_exit_ca + t_show_cursor + t_hide_cursor + t_clear_screen + t_sgr0 + t_underline + t_bold + t_blink + t_reverse + t_enter_keypad + t_exit_keypad + t_enter_mouse + t_exit_mouse + t_max_funcs +) + +const ( + coord_invalid = -2 + attr_invalid = Attribute(0xFFFF) +) + +type input_event struct { + data []byte + err error +} + +var ( + // term specific sequences + keys []string + funcs []string + + // termbox inner state + orig_tios syscall_Termios + back_buffer cellbuf + front_buffer cellbuf + termw int + termh int + input_mode = InputEsc + output_mode = OutputNormal + out *os.File + in int + lastfg = attr_invalid + lastbg = attr_invalid + lastx = coord_invalid + lasty = coord_invalid + cursor_x = cursor_hidden + cursor_y = cursor_hidden + foreground = ColorDefault + background = ColorDefault + inbuf = make([]byte, 0, 64) + outbuf bytes.Buffer + sigwinch = make(chan os.Signal, 1) + sigio = make(chan os.Signal, 1) + quit = make(chan int) + input_comm = make(chan input_event) + interrupt_comm = make(chan struct{}) + intbuf = make([]byte, 0, 16) +) + +func write_cursor(x, y int) { + outbuf.WriteString("\033[") + outbuf.Write(strconv.AppendUint(intbuf, uint64(y+1), 10)) + outbuf.WriteString(";") + outbuf.Write(strconv.AppendUint(intbuf, uint64(x+1), 10)) + outbuf.WriteString("H") +} + +func write_sgr_fg(a Attribute) { + switch output_mode { + case Output256, Output216, OutputGrayscale: + outbuf.WriteString("\033[38;5;") + outbuf.Write(strconv.AppendUint(intbuf, uint64(a-1), 10)) + outbuf.WriteString("m") + default: + outbuf.WriteString("\033[3") + outbuf.Write(strconv.AppendUint(intbuf, uint64(a-1), 10)) + outbuf.WriteString("m") + } +} + +func write_sgr_bg(a Attribute) { + switch output_mode { + case Output256, Output216, OutputGrayscale: + outbuf.WriteString("\033[48;5;") + outbuf.Write(strconv.AppendUint(intbuf, uint64(a-1), 10)) + outbuf.WriteString("m") + default: + outbuf.WriteString("\033[4") + outbuf.Write(strconv.AppendUint(intbuf, uint64(a-1), 10)) + outbuf.WriteString("m") + } +} + +func write_sgr(fg, bg Attribute) { + switch output_mode { + case Output256, Output216, OutputGrayscale: + outbuf.WriteString("\033[38;5;") + outbuf.Write(strconv.AppendUint(intbuf, uint64(fg-1), 10)) + outbuf.WriteString("m") + outbuf.WriteString("\033[48;5;") + outbuf.Write(strconv.AppendUint(intbuf, uint64(bg-1), 10)) + outbuf.WriteString("m") + default: + outbuf.WriteString("\033[3") + outbuf.Write(strconv.AppendUint(intbuf, uint64(fg-1), 10)) + outbuf.WriteString(";4") + outbuf.Write(strconv.AppendUint(intbuf, uint64(bg-1), 10)) + outbuf.WriteString("m") + } +} + +type winsize struct { + rows uint16 + cols uint16 + xpixels uint16 + ypixels uint16 +} + +func get_term_size(fd uintptr) (int, int) { + var sz winsize + _, _, _ = syscall.Syscall(syscall.SYS_IOCTL, + fd, uintptr(syscall.TIOCGWINSZ), uintptr(unsafe.Pointer(&sz))) + return int(sz.cols), int(sz.rows) +} + +func send_attr(fg, bg Attribute) { + if fg == lastfg && bg == lastbg { + return + } + + outbuf.WriteString(funcs[t_sgr0]) + + var fgcol, bgcol Attribute + + switch output_mode { + case Output256: + fgcol = fg & 0x1FF + bgcol = bg & 0x1FF + case Output216: + fgcol = fg & 0xFF + bgcol = bg & 0xFF + if fgcol > 216 { + fgcol = ColorDefault + } + if bgcol > 216 { + bgcol = ColorDefault + } + if fgcol != ColorDefault { + fgcol += 0x10 + } + if bgcol != ColorDefault { + bgcol += 0x10 + } + case OutputGrayscale: + fgcol = fg & 0x1F + bgcol = bg & 0x1F + if fgcol > 24 { + fgcol = ColorDefault + } + if bgcol > 24 { + bgcol = ColorDefault + } + if fgcol != ColorDefault { + fgcol += 0xe8 + } + if bgcol != ColorDefault { + bgcol += 0xe8 + } + default: + fgcol = fg & 0x0F + bgcol = bg & 0x0F + } + + if fgcol != ColorDefault { + if bgcol != ColorDefault { + write_sgr(fgcol, bgcol) + } else { + write_sgr_fg(fgcol) + } + } else if bgcol != ColorDefault { + write_sgr_bg(bgcol) + } + + if fg&AttrBold != 0 { + outbuf.WriteString(funcs[t_bold]) + } + if bg&AttrBold != 0 { + outbuf.WriteString(funcs[t_blink]) + } + if fg&AttrUnderline != 0 { + outbuf.WriteString(funcs[t_underline]) + } + if fg&AttrReverse|bg&AttrReverse != 0 { + outbuf.WriteString(funcs[t_reverse]) + } + + lastfg, lastbg = fg, bg +} + +func send_char(x, y int, ch rune) { + var buf [8]byte + n := utf8.EncodeRune(buf[:], ch) + if x-1 != lastx || y != lasty { + write_cursor(x, y) + } + lastx, lasty = x, y + outbuf.Write(buf[:n]) +} + +func flush() error { + _, err := io.Copy(out, &outbuf) + outbuf.Reset() + if err != nil { + return err + } + return nil +} + +func send_clear() error { + send_attr(foreground, background) + outbuf.WriteString(funcs[t_clear_screen]) + if !is_cursor_hidden(cursor_x, cursor_y) { + write_cursor(cursor_x, cursor_y) + } + + // we need to invalidate cursor position too and these two vars are + // used only for simple cursor positioning optimization, cursor + // actually may be in the correct place, but we simply discard + // optimization once and it gives us simple solution for the case when + // cursor moved + lastx = coord_invalid + lasty = coord_invalid + + return flush() +} + +func update_size_maybe() error { + w, h := get_term_size(out.Fd()) + if w != termw || h != termh { + termw, termh = w, h + back_buffer.resize(termw, termh) + front_buffer.resize(termw, termh) + front_buffer.clear() + return send_clear() + } + return nil +} + +func tcsetattr(fd uintptr, termios *syscall_Termios) error { + r, _, e := syscall.Syscall(syscall.SYS_IOCTL, + fd, uintptr(syscall_TCSETS), uintptr(unsafe.Pointer(termios))) + if r != 0 { + return os.NewSyscallError("SYS_IOCTL", e) + } + return nil +} + +func tcgetattr(fd uintptr, termios *syscall_Termios) error { + r, _, e := syscall.Syscall(syscall.SYS_IOCTL, + fd, uintptr(syscall_TCGETS), uintptr(unsafe.Pointer(termios))) + if r != 0 { + return os.NewSyscallError("SYS_IOCTL", e) + } + return nil +} + +func parse_escape_sequence(event *Event, buf []byte) (int, bool) { + bufstr := string(buf) + // mouse + if len(bufstr) >= 6 && strings.HasPrefix(bufstr, "\033[M") { + switch buf[3] & 3 { + case 0: + event.Key = MouseLeft + case 1: + event.Key = MouseMiddle + case 2: + event.Key = MouseRight + case 3: + return 6, false + } + event.Type = EventMouse // KeyEvent by default + // wheel up outputs MouseLeft + if buf[3] == 0x60 || buf[3] == 0x70 { + event.Key = MouseMiddle + } + // the coord is 1,1 for upper left + event.MouseX = int(buf[4]) - 1 - 32 + event.MouseY = int(buf[5]) - 1 - 32 + return 6, true + } + + for i, key := range keys { + if strings.HasPrefix(bufstr, key) { + event.Ch = 0 + event.Key = Key(0xFFFF - i) + return len(key), true + } + } + return 0, true +} + +func extract_raw_event(data []byte, event *Event) bool { + if len(inbuf) == 0 { + return false + } + + n := len(data) + if n == 0 { + return false + } + + n = copy(data, inbuf) + copy(inbuf, inbuf[n:]) + inbuf = inbuf[:len(inbuf)-n] + + event.N = n + event.Type = EventRaw + return true +} + +func extract_event(inbuf []byte, event *Event) bool { + if len(inbuf) == 0 { + event.N = 0 + return false + } + + if inbuf[0] == '\033' { + // possible escape sequence + n, ok := parse_escape_sequence(event, inbuf) + if n != 0 { + event.N = n + return ok + } + + // it's not escape sequence, then it's Alt or Esc, check input_mode + switch { + case input_mode&InputEsc != 0: + // if we're in escape mode, fill Esc event, pop buffer, return success + event.Ch = 0 + event.Key = KeyEsc + event.Mod = 0 + event.N = 1 + return true + case input_mode&InputAlt != 0: + // if we're in alt mode, set Alt modifier to event and redo parsing + event.Mod = ModAlt + ok := extract_event(inbuf[1:], event) + if ok { + event.N++ + } else { + event.N = 0 + } + return ok + default: + panic("unreachable") + } + } + + // if we're here, this is not an escape sequence and not an alt sequence + // so, it's a FUNCTIONAL KEY or a UNICODE character + + // first of all check if it's a functional key + if Key(inbuf[0]) <= KeySpace || Key(inbuf[0]) == KeyBackspace2 { + // fill event, pop buffer, return success + event.Ch = 0 + event.Key = Key(inbuf[0]) + event.N = 1 + return true + } + + // the only possible option is utf8 rune + if r, n := utf8.DecodeRune(inbuf); r != utf8.RuneError { + event.Ch = r + event.Key = 0 + event.N = n + return true + } + + return false +} + +func fcntl(fd int, cmd int, arg int) (val int, err error) { + r, _, e := syscall.Syscall(syscall.SYS_FCNTL, uintptr(fd), uintptr(cmd), + uintptr(arg)) + val = int(r) + if e != 0 { + err = e + } + return +} diff --git a/Godeps/_workspace/src/github.com/nsf/termbox-go/termbox_common.go b/Godeps/_workspace/src/github.com/nsf/termbox-go/termbox_common.go new file mode 100644 index 0000000..c3355cc --- /dev/null +++ b/Godeps/_workspace/src/github.com/nsf/termbox-go/termbox_common.go @@ -0,0 +1,59 @@ +package termbox + +// private API, common OS agnostic part + +type cellbuf struct { + width int + height int + cells []Cell +} + +func (this *cellbuf) init(width, height int) { + this.width = width + this.height = height + this.cells = make([]Cell, width*height) +} + +func (this *cellbuf) resize(width, height int) { + if this.width == width && this.height == height { + return + } + + oldw := this.width + oldh := this.height + oldcells := this.cells + + this.init(width, height) + this.clear() + + minw, minh := oldw, oldh + + if width < minw { + minw = width + } + if height < minh { + minh = height + } + + for i := 0; i < minh; i++ { + srco, dsto := i*oldw, i*width + src := oldcells[srco : srco+minw] + dst := this.cells[dsto : dsto+minw] + copy(dst, src) + } +} + +func (this *cellbuf) clear() { + for i := range this.cells { + c := &this.cells[i] + c.Ch = ' ' + c.Fg = foreground + c.Bg = background + } +} + +const cursor_hidden = -1 + +func is_cursor_hidden(x, y int) bool { + return x == cursor_hidden || y == cursor_hidden +} diff --git a/Godeps/_workspace/src/github.com/nsf/termbox-go/termbox_windows.go b/Godeps/_workspace/src/github.com/nsf/termbox-go/termbox_windows.go new file mode 100644 index 0000000..10906b7 --- /dev/null +++ b/Godeps/_workspace/src/github.com/nsf/termbox-go/termbox_windows.go @@ -0,0 +1,812 @@ +package termbox + +import "syscall" +import "unsafe" +import "unicode/utf16" +import "github.com/mattn/go-runewidth" + +type ( + wchar uint16 + short int16 + dword uint32 + word uint16 + char_info struct { + char wchar + attr word + } + coord struct { + x short + y short + } + small_rect struct { + left short + top short + right short + bottom short + } + console_screen_buffer_info struct { + size coord + cursor_position coord + attributes word + window small_rect + maximum_window_size coord + } + console_cursor_info struct { + size dword + visible int32 + } + input_record struct { + event_type word + _ [2]byte + event [16]byte + } + key_event_record struct { + key_down int32 + repeat_count word + virtual_key_code word + virtual_scan_code word + unicode_char wchar + control_key_state dword + } + window_buffer_size_record struct { + size coord + } + mouse_event_record struct { + mouse_pos coord + button_state dword + control_key_state dword + event_flags dword + } +) + +const ( + mouse_lmb = 0x1 + mouse_rmb = 0x2 + mouse_mmb = 0x4 | 0x8 | 0x10 +) + +func (this coord) uintptr() uintptr { + return uintptr(*(*int32)(unsafe.Pointer(&this))) +} + +var kernel32 = syscall.NewLazyDLL("kernel32.dll") +var is_cjk = runewidth.IsEastAsian() + +var ( + proc_set_console_active_screen_buffer = kernel32.NewProc("SetConsoleActiveScreenBuffer") + proc_set_console_screen_buffer_size = kernel32.NewProc("SetConsoleScreenBufferSize") + proc_create_console_screen_buffer = kernel32.NewProc("CreateConsoleScreenBuffer") + proc_get_console_screen_buffer_info = kernel32.NewProc("GetConsoleScreenBufferInfo") + proc_write_console_output = kernel32.NewProc("WriteConsoleOutputW") + proc_write_console_output_character = kernel32.NewProc("WriteConsoleOutputCharacterW") + proc_write_console_output_attribute = kernel32.NewProc("WriteConsoleOutputAttribute") + proc_set_console_cursor_info = kernel32.NewProc("SetConsoleCursorInfo") + proc_set_console_cursor_position = kernel32.NewProc("SetConsoleCursorPosition") + proc_get_console_cursor_info = kernel32.NewProc("GetConsoleCursorInfo") + proc_read_console_input = kernel32.NewProc("ReadConsoleInputW") + proc_get_console_mode = kernel32.NewProc("GetConsoleMode") + proc_set_console_mode = kernel32.NewProc("SetConsoleMode") + proc_fill_console_output_character = kernel32.NewProc("FillConsoleOutputCharacterW") + proc_fill_console_output_attribute = kernel32.NewProc("FillConsoleOutputAttribute") + proc_create_event = kernel32.NewProc("CreateEventW") + proc_wait_for_multiple_objects = kernel32.NewProc("WaitForMultipleObjects") + proc_set_event = kernel32.NewProc("SetEvent") +) + +func set_console_active_screen_buffer(h syscall.Handle) (err error) { + r0, _, e1 := syscall.Syscall(proc_set_console_active_screen_buffer.Addr(), + 1, uintptr(h), 0, 0) + if int(r0) == 0 { + if e1 != 0 { + err = error(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func set_console_screen_buffer_size(h syscall.Handle, size coord) (err error) { + r0, _, e1 := syscall.Syscall(proc_set_console_screen_buffer_size.Addr(), + 2, uintptr(h), size.uintptr(), 0) + if int(r0) == 0 { + if e1 != 0 { + err = error(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func create_console_screen_buffer() (h syscall.Handle, err error) { + r0, _, e1 := syscall.Syscall6(proc_create_console_screen_buffer.Addr(), + 5, uintptr(generic_read|generic_write), 0, 0, console_textmode_buffer, 0, 0) + if int(r0) == 0 { + if e1 != 0 { + err = error(e1) + } else { + err = syscall.EINVAL + } + } + return syscall.Handle(r0), nil +} + +func get_console_screen_buffer_info(h syscall.Handle, info *console_screen_buffer_info) (err error) { + r0, _, e1 := syscall.Syscall(proc_get_console_screen_buffer_info.Addr(), + 2, uintptr(h), uintptr(unsafe.Pointer(info)), 0) + if int(r0) == 0 { + if e1 != 0 { + err = error(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func write_console_output(h syscall.Handle, chars []char_info, dst small_rect) (err error) { + tmp_coord = coord{dst.right - dst.left + 1, dst.bottom - dst.top + 1} + tmp_rect = dst + r0, _, e1 := syscall.Syscall6(proc_write_console_output.Addr(), + 5, uintptr(h), uintptr(unsafe.Pointer(&chars[0])), tmp_coord.uintptr(), + tmp_coord0.uintptr(), uintptr(unsafe.Pointer(&tmp_rect)), 0) + if int(r0) == 0 { + if e1 != 0 { + err = error(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func write_console_output_character(h syscall.Handle, chars []wchar, pos coord) (err error) { + r0, _, e1 := syscall.Syscall6(proc_write_console_output_character.Addr(), + 5, uintptr(h), uintptr(unsafe.Pointer(&chars[0])), uintptr(len(chars)), + pos.uintptr(), uintptr(unsafe.Pointer(&tmp_arg)), 0) + if int(r0) == 0 { + if e1 != 0 { + err = error(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func write_console_output_attribute(h syscall.Handle, attrs []word, pos coord) (err error) { + r0, _, e1 := syscall.Syscall6(proc_write_console_output_attribute.Addr(), + 5, uintptr(h), uintptr(unsafe.Pointer(&attrs[0])), uintptr(len(attrs)), + pos.uintptr(), uintptr(unsafe.Pointer(&tmp_arg)), 0) + if int(r0) == 0 { + if e1 != 0 { + err = error(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func set_console_cursor_info(h syscall.Handle, info *console_cursor_info) (err error) { + r0, _, e1 := syscall.Syscall(proc_set_console_cursor_info.Addr(), + 2, uintptr(h), uintptr(unsafe.Pointer(info)), 0) + if int(r0) == 0 { + if e1 != 0 { + err = error(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func get_console_cursor_info(h syscall.Handle, info *console_cursor_info) (err error) { + r0, _, e1 := syscall.Syscall(proc_get_console_cursor_info.Addr(), + 2, uintptr(h), uintptr(unsafe.Pointer(info)), 0) + if int(r0) == 0 { + if e1 != 0 { + err = error(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func set_console_cursor_position(h syscall.Handle, pos coord) (err error) { + r0, _, e1 := syscall.Syscall(proc_set_console_cursor_position.Addr(), + 2, uintptr(h), pos.uintptr(), 0) + if int(r0) == 0 { + if e1 != 0 { + err = error(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func read_console_input(h syscall.Handle, record *input_record) (err error) { + r0, _, e1 := syscall.Syscall6(proc_read_console_input.Addr(), + 4, uintptr(h), uintptr(unsafe.Pointer(record)), 1, uintptr(unsafe.Pointer(&tmp_arg)), 0, 0) + if int(r0) == 0 { + if e1 != 0 { + err = error(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func get_console_mode(h syscall.Handle, mode *dword) (err error) { + r0, _, e1 := syscall.Syscall(proc_get_console_mode.Addr(), + 2, uintptr(h), uintptr(unsafe.Pointer(mode)), 0) + if int(r0) == 0 { + if e1 != 0 { + err = error(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func set_console_mode(h syscall.Handle, mode dword) (err error) { + r0, _, e1 := syscall.Syscall(proc_set_console_mode.Addr(), + 2, uintptr(h), uintptr(mode), 0) + if int(r0) == 0 { + if e1 != 0 { + err = error(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func fill_console_output_character(h syscall.Handle, char wchar, n int) (err error) { + r0, _, e1 := syscall.Syscall6(proc_fill_console_output_character.Addr(), + 5, uintptr(h), uintptr(char), uintptr(n), tmp_coord.uintptr(), + uintptr(unsafe.Pointer(&tmp_arg)), 0) + if int(r0) == 0 { + if e1 != 0 { + err = error(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func fill_console_output_attribute(h syscall.Handle, attr word, n int) (err error) { + r0, _, e1 := syscall.Syscall6(proc_fill_console_output_attribute.Addr(), + 5, uintptr(h), uintptr(attr), uintptr(n), tmp_coord.uintptr(), + uintptr(unsafe.Pointer(&tmp_arg)), 0) + if int(r0) == 0 { + if e1 != 0 { + err = error(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func create_event() (out syscall.Handle, err error) { + r0, _, e1 := syscall.Syscall6(proc_create_event.Addr(), + 4, 0, 0, 0, 0, 0, 0) + if int(r0) == 0 { + if e1 != 0 { + err = error(e1) + } else { + err = syscall.EINVAL + } + } + return syscall.Handle(r0), nil +} + +func wait_for_multiple_objects(objects []syscall.Handle) (err error) { + r0, _, e1 := syscall.Syscall6(proc_wait_for_multiple_objects.Addr(), + 4, uintptr(len(objects)), uintptr(unsafe.Pointer(&objects[0])), + 0, 0xFFFFFFFF, 0, 0) + if uint32(r0) == 0xFFFFFFFF { + if e1 != 0 { + err = error(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +func set_event(ev syscall.Handle) (err error) { + r0, _, e1 := syscall.Syscall(proc_set_event.Addr(), + 1, uintptr(ev), 0, 0) + if int(r0) == 0 { + if e1 != 0 { + err = error(e1) + } else { + err = syscall.EINVAL + } + } + return +} + +type diff_msg struct { + pos short + lines short + chars []char_info +} + +type input_event struct { + event Event + err error +} + +var ( + orig_cursor_info console_cursor_info + orig_size coord + orig_mode dword + orig_screen syscall.Handle + back_buffer cellbuf + front_buffer cellbuf + term_size coord + input_mode = InputEsc + cursor_x = cursor_hidden + cursor_y = cursor_hidden + foreground = ColorDefault + background = ColorDefault + in syscall.Handle + out syscall.Handle + interrupt syscall.Handle + charbuf []char_info + diffbuf []diff_msg + beg_x = -1 + beg_y = -1 + beg_i = -1 + input_comm = make(chan Event) + interrupt_comm = make(chan struct{}) + cancel_comm = make(chan bool, 1) + cancel_done_comm = make(chan bool) + alt_mode_esc = false + + // these ones just to prevent heap allocs at all costs + tmp_info console_screen_buffer_info + tmp_arg dword + tmp_coord0 = coord{0, 0} + tmp_coord = coord{0, 0} + tmp_rect = small_rect{0, 0, 0, 0} +) + +func get_cursor_position(out syscall.Handle) coord { + err := get_console_screen_buffer_info(out, &tmp_info) + if err != nil { + panic(err) + } + return tmp_info.cursor_position +} + +func get_term_size(out syscall.Handle) coord { + err := get_console_screen_buffer_info(out, &tmp_info) + if err != nil { + panic(err) + } + return tmp_info.size +} + +func get_win_size(out syscall.Handle) coord { + err := get_console_screen_buffer_info(out, &tmp_info) + if err != nil { + panic(err) + } + return coord{ + x: tmp_info.window.right - tmp_info.window.left + 1, + y: tmp_info.window.bottom - tmp_info.window.top + 1, + } +} + +func update_size_maybe() { + size := get_term_size(out) + if size.x != term_size.x || size.y != term_size.y { + term_size = size + back_buffer.resize(int(size.x), int(size.y)) + front_buffer.resize(int(size.x), int(size.y)) + front_buffer.clear() + clear() + + area := int(size.x) * int(size.y) + if cap(charbuf) < area { + charbuf = make([]char_info, 0, area) + } + } +} + +var color_table_bg = []word{ + 0, // default (black) + 0, // black + background_red, + background_green, + background_red | background_green, // yellow + background_blue, + background_red | background_blue, // magenta + background_green | background_blue, // cyan + background_red | background_blue | background_green, // white +} + +var color_table_fg = []word{ + foreground_red | foreground_blue | foreground_green, // default (white) + 0, + foreground_red, + foreground_green, + foreground_red | foreground_green, // yellow + foreground_blue, + foreground_red | foreground_blue, // magenta + foreground_green | foreground_blue, // cyan + foreground_red | foreground_blue | foreground_green, // white +} + +const ( + replacement_char = '\uFFFD' + max_rune = '\U0010FFFF' + surr1 = 0xd800 + surr2 = 0xdc00 + surr3 = 0xe000 + surr_self = 0x10000 +) + +func append_diff_line(y int) int { + n := 0 + for x := 0; x < front_buffer.width; { + cell_offset := y*front_buffer.width + x + back := &back_buffer.cells[cell_offset] + front := &front_buffer.cells[cell_offset] + attr, char := cell_to_char_info(*back) + charbuf = append(charbuf, char_info{attr: attr, char: char[0]}) + *front = *back + n++ + w := runewidth.RuneWidth(back.Ch) + if w == 0 || w == 2 && runewidth.IsAmbiguousWidth(back.Ch) { + w = 1 + } + x += w + // If not CJK, fill trailing space with whitespace + if !is_cjk && w == 2 { + charbuf = append(charbuf, char_info{attr: attr, char: ' '}) + } + } + return n +} + +// compares 'back_buffer' with 'front_buffer' and prepares all changes in the form of +// 'diff_msg's in the 'diff_buf' +func prepare_diff_messages() { + // clear buffers + diffbuf = diffbuf[:0] + + var diff diff_msg + gbeg := 0 + for y := 0; y < front_buffer.height; y++ { + same := true + line_offset := y * front_buffer.width + for x := 0; x < front_buffer.width; x++ { + cell_offset := line_offset + x + back := &back_buffer.cells[cell_offset] + front := &front_buffer.cells[cell_offset] + if *back != *front { + same = false + break + } + } + if same && diff.lines > 0 { + diffbuf = append(diffbuf, diff) + diff = diff_msg{} + } + if !same { + beg := len(charbuf) + end := beg + append_diff_line(y) + if diff.lines == 0 { + diff.pos = short(y) + gbeg = beg + } + diff.lines++ + diff.chars = charbuf[gbeg:end] + } + } + if diff.lines > 0 { + diffbuf = append(diffbuf, diff) + diff = diff_msg{} + } +} + +func cell_to_char_info(c Cell) (attr word, wc [2]wchar) { + attr = color_table_fg[c.Fg&0x0F] | color_table_bg[c.Bg&0x0F] + if c.Fg&AttrReverse|c.Bg&AttrReverse != 0 { + attr = (attr&0xF0)>>4 | (attr&0x0F)<<4 + } + if c.Fg&AttrBold != 0 { + attr |= foreground_intensity + } + if c.Bg&AttrBold != 0 { + attr |= background_intensity + } + + r0, r1 := utf16.EncodeRune(c.Ch) + if r0 == 0xFFFD { + wc[0] = wchar(c.Ch) + wc[1] = ' ' + } else { + wc[0] = wchar(r0) + wc[1] = wchar(r1) + } + return +} + +func move_cursor(x, y int) { + err := set_console_cursor_position(out, coord{short(x), short(y)}) + if err != nil { + panic(err) + } +} + +func show_cursor(visible bool) { + var v int32 + if visible { + v = 1 + } + + var info console_cursor_info + info.size = 100 + info.visible = v + err := set_console_cursor_info(out, &info) + if err != nil { + panic(err) + } +} + +func clear() { + var err error + attr, char := cell_to_char_info(Cell{ + ' ', + foreground, + background, + }) + + area := int(term_size.x) * int(term_size.y) + err = fill_console_output_attribute(out, attr, area) + if err != nil { + panic(err) + } + err = fill_console_output_character(out, char[0], area) + if err != nil { + panic(err) + } + if !is_cursor_hidden(cursor_x, cursor_y) { + move_cursor(cursor_x, cursor_y) + } +} + +func key_event_record_to_event(r *key_event_record) (Event, bool) { + if r.key_down == 0 { + return Event{}, false + } + + e := Event{Type: EventKey} + if input_mode&InputAlt != 0 { + if alt_mode_esc { + e.Mod = ModAlt + alt_mode_esc = false + } + if r.control_key_state&(left_alt_pressed|right_alt_pressed) != 0 { + e.Mod = ModAlt + } + } + + ctrlpressed := r.control_key_state&(left_ctrl_pressed|right_ctrl_pressed) != 0 + + if r.virtual_key_code >= vk_f1 && r.virtual_key_code <= vk_f12 { + switch r.virtual_key_code { + case vk_f1: + e.Key = KeyF1 + case vk_f2: + e.Key = KeyF2 + case vk_f3: + e.Key = KeyF3 + case vk_f4: + e.Key = KeyF4 + case vk_f5: + e.Key = KeyF5 + case vk_f6: + e.Key = KeyF6 + case vk_f7: + e.Key = KeyF7 + case vk_f8: + e.Key = KeyF8 + case vk_f9: + e.Key = KeyF9 + case vk_f10: + e.Key = KeyF10 + case vk_f11: + e.Key = KeyF11 + case vk_f12: + e.Key = KeyF12 + default: + panic("unreachable") + } + + return e, true + } + + if r.virtual_key_code <= vk_delete { + switch r.virtual_key_code { + case vk_insert: + e.Key = KeyInsert + case vk_delete: + e.Key = KeyDelete + case vk_home: + e.Key = KeyHome + case vk_end: + e.Key = KeyEnd + case vk_pgup: + e.Key = KeyPgup + case vk_pgdn: + e.Key = KeyPgdn + case vk_arrow_up: + e.Key = KeyArrowUp + case vk_arrow_down: + e.Key = KeyArrowDown + case vk_arrow_left: + e.Key = KeyArrowLeft + case vk_arrow_right: + e.Key = KeyArrowRight + case vk_backspace: + if ctrlpressed { + e.Key = KeyBackspace2 + } else { + e.Key = KeyBackspace + } + case vk_tab: + e.Key = KeyTab + case vk_enter: + e.Key = KeyEnter + case vk_esc: + switch { + case input_mode&InputEsc != 0: + e.Key = KeyEsc + case input_mode&InputAlt != 0: + alt_mode_esc = true + return Event{}, false + } + case vk_space: + if ctrlpressed { + // manual return here, because KeyCtrlSpace is zero + e.Key = KeyCtrlSpace + return e, true + } else { + e.Key = KeySpace + } + } + + if e.Key != 0 { + return e, true + } + } + + if ctrlpressed { + if Key(r.unicode_char) >= KeyCtrlA && Key(r.unicode_char) <= KeyCtrlRsqBracket { + e.Key = Key(r.unicode_char) + if input_mode&InputAlt != 0 && e.Key == KeyEsc { + alt_mode_esc = true + return Event{}, false + } + return e, true + } + switch r.virtual_key_code { + case 192, 50: + // manual return here, because KeyCtrl2 is zero + e.Key = KeyCtrl2 + return e, true + case 51: + if input_mode&InputAlt != 0 { + alt_mode_esc = true + return Event{}, false + } + e.Key = KeyCtrl3 + case 52: + e.Key = KeyCtrl4 + case 53: + e.Key = KeyCtrl5 + case 54: + e.Key = KeyCtrl6 + case 189, 191, 55: + e.Key = KeyCtrl7 + case 8, 56: + e.Key = KeyCtrl8 + } + + if e.Key != 0 { + return e, true + } + } + + if r.unicode_char != 0 { + e.Ch = rune(r.unicode_char) + return e, true + } + + return Event{}, false +} + +func input_event_producer() { + var r input_record + var err error + var last_button Key + var last_state = dword(0) + handles := []syscall.Handle{in, interrupt} + for { + err = wait_for_multiple_objects(handles) + if err != nil { + input_comm <- Event{Type: EventError, Err: err} + } + + select { + case <-cancel_comm: + cancel_done_comm <- true + return + default: + } + + err = read_console_input(in, &r) + if err != nil { + input_comm <- Event{Type: EventError, Err: err} + } + + switch r.event_type { + case key_event: + kr := (*key_event_record)(unsafe.Pointer(&r.event)) + ev, ok := key_event_record_to_event(kr) + if ok { + for i := 0; i < int(kr.repeat_count); i++ { + input_comm <- ev + } + } + case window_buffer_size_event: + sr := *(*window_buffer_size_record)(unsafe.Pointer(&r.event)) + input_comm <- Event{ + Type: EventResize, + Width: int(sr.size.x), + Height: int(sr.size.y), + } + case mouse_event: + mr := *(*mouse_event_record)(unsafe.Pointer(&r.event)) + + // single or double click + switch mr.event_flags { + case 0: + cur_state := mr.button_state + switch { + case last_state&mouse_lmb == 0 && cur_state&mouse_lmb != 0: + last_button = MouseLeft + case last_state&mouse_rmb == 0 && cur_state&mouse_rmb != 0: + last_button = MouseRight + case last_state&mouse_mmb == 0 && cur_state&mouse_mmb != 0: + last_button = MouseMiddle + default: + last_state = cur_state + continue + } + last_state = cur_state + fallthrough + case 2: + input_comm <- Event{ + Type: EventMouse, + Key: last_button, + MouseX: int(mr.mouse_pos.x), + MouseY: int(mr.mouse_pos.y), + } + } + } + } +} diff --git a/Godeps/_workspace/src/github.com/nsf/termbox-go/terminfo.go b/Godeps/_workspace/src/github.com/nsf/termbox-go/terminfo.go new file mode 100644 index 0000000..3569e3c --- /dev/null +++ b/Godeps/_workspace/src/github.com/nsf/termbox-go/terminfo.go @@ -0,0 +1,219 @@ +// +build !windows +// This file contains a simple and incomplete implementation of the terminfo +// database. Information was taken from the ncurses manpages term(5) and +// terminfo(5). Currently, only the string capabilities for special keys and for +// functions without parameters are actually used. Colors are still done with +// ANSI escape sequences. Other special features that are not (yet?) supported +// are reading from ~/.terminfo, the TERMINFO_DIRS variable, Berkeley database +// format and extended capabilities. + +package termbox + +import ( + "bytes" + "encoding/binary" + "encoding/hex" + "errors" + "fmt" + "io/ioutil" + "os" + "strings" +) + +const ( + ti_magic = 0432 + ti_header_length = 12 +) + +func load_terminfo() ([]byte, error) { + var data []byte + var err error + + term := os.Getenv("TERM") + if term == "" { + return nil, fmt.Errorf("termbox: TERM not set") + } + + // The following behaviour follows the one described in terminfo(5) as + // distributed by ncurses. + + terminfo := os.Getenv("TERMINFO") + if terminfo != "" { + // if TERMINFO is set, no other directory should be searched + return ti_try_path(terminfo) + } + + // next, consider ~/.terminfo + home := os.Getenv("HOME") + if home != "" { + data, err = ti_try_path(home + "/.terminfo") + if err == nil { + return data, nil + } + } + + // next, TERMINFO_DIRS + dirs := os.Getenv("TERMINFO_DIRS") + if dirs != "" { + for _, dir := range strings.Split(dirs, ":") { + if dir == "" { + // "" -> "/usr/share/terminfo" + dir = "/usr/share/terminfo" + } + data, err = ti_try_path(dir) + if err == nil { + return data, nil + } + } + } + + // fall back to /usr/share/terminfo + return ti_try_path("/usr/share/terminfo") +} + +func ti_try_path(path string) (data []byte, err error) { + // load_terminfo already made sure it is set + term := os.Getenv("TERM") + + // first try, the typical *nix path + terminfo := path + "/" + term[0:1] + "/" + term + data, err = ioutil.ReadFile(terminfo) + if err == nil { + return + } + + // fallback to darwin specific dirs structure + terminfo = path + "/" + hex.EncodeToString([]byte(term[:1])) + "/" + term + data, err = ioutil.ReadFile(terminfo) + return +} + +func setup_term_builtin() error { + name := os.Getenv("TERM") + if name == "" { + return errors.New("termbox: TERM environment variable not set") + } + + for _, t := range terms { + if t.name == name { + keys = t.keys + funcs = t.funcs + return nil + } + } + + compat_table := []struct { + partial string + keys []string + funcs []string + }{ + {"xterm", xterm_keys, xterm_funcs}, + {"rxvt", rxvt_unicode_keys, rxvt_unicode_funcs}, + {"linux", linux_keys, linux_funcs}, + {"Eterm", eterm_keys, eterm_funcs}, + {"screen", screen_keys, screen_funcs}, + // let's assume that 'cygwin' is xterm compatible + {"cygwin", xterm_keys, xterm_funcs}, + {"st", xterm_keys, xterm_funcs}, + } + + // try compatibility variants + for _, it := range compat_table { + if strings.Contains(name, it.partial) { + keys = it.keys + funcs = it.funcs + return nil + } + } + + return errors.New("termbox: unsupported terminal") +} + +func setup_term() (err error) { + var data []byte + var header [6]int16 + var str_offset, table_offset int16 + + data, err = load_terminfo() + if err != nil { + return setup_term_builtin() + } + + rd := bytes.NewReader(data) + // 0: magic number, 1: size of names section, 2: size of boolean section, 3: + // size of numbers section (in integers), 4: size of the strings section (in + // integers), 5: size of the string table + + err = binary.Read(rd, binary.LittleEndian, header[:]) + if err != nil { + return + } + + if (header[1]+header[2])%2 != 0 { + // old quirk to align everything on word boundaries + header[2] += 1 + } + str_offset = ti_header_length + header[1] + header[2] + 2*header[3] + table_offset = str_offset + 2*header[4] + + keys = make([]string, 0xFFFF-key_min) + for i, _ := range keys { + keys[i], err = ti_read_string(rd, str_offset+2*ti_keys[i], table_offset) + if err != nil { + return + } + } + funcs = make([]string, t_max_funcs) + // the last two entries are reserved for mouse. because the table offset is + // not there, the two entries have to fill in manually + for i, _ := range funcs[:len(funcs)-2] { + funcs[i], err = ti_read_string(rd, str_offset+2*ti_funcs[i], table_offset) + if err != nil { + return + } + } + funcs[t_max_funcs-2] = "\x1b[?1000h" + funcs[t_max_funcs-1] = "\x1b[?1000l" + return nil +} + +func ti_read_string(rd *bytes.Reader, str_off, table int16) (string, error) { + var off int16 + + _, err := rd.Seek(int64(str_off), 0) + if err != nil { + return "", err + } + err = binary.Read(rd, binary.LittleEndian, &off) + if err != nil { + return "", err + } + _, err = rd.Seek(int64(table+off), 0) + if err != nil { + return "", err + } + var bs []byte + for { + b, err := rd.ReadByte() + if err != nil { + return "", err + } + if b == byte(0x00) { + break + } + bs = append(bs, b) + } + return string(bs), nil +} + +// "Maps" the function constants from termbox.go to the number of the respective +// string capability in the terminfo file. Taken from (ncurses) term.h. +var ti_funcs = []int16{ + 28, 40, 16, 13, 5, 39, 36, 27, 26, 34, 89, 88, +} + +// Same as above for the special keys. +var ti_keys = []int16{ + 66, 68 /* apparently not a typo; 67 is F10 for whatever reason */, 69, 70, + 71, 72, 73, 74, 75, 67, 216, 217, 77, 59, 76, 164, 82, 81, 87, 61, 79, 83, +} diff --git a/Godeps/_workspace/src/github.com/nsf/termbox-go/terminfo_builtin.go b/Godeps/_workspace/src/github.com/nsf/termbox-go/terminfo_builtin.go new file mode 100644 index 0000000..6f927c8 --- /dev/null +++ b/Godeps/_workspace/src/github.com/nsf/termbox-go/terminfo_builtin.go @@ -0,0 +1,64 @@ +// +build !windows + +package termbox + +// Eterm +var eterm_keys = []string{ + "\x1b[11~", "\x1b[12~", "\x1b[13~", "\x1b[14~", "\x1b[15~", "\x1b[17~", "\x1b[18~", "\x1b[19~", "\x1b[20~", "\x1b[21~", "\x1b[23~", "\x1b[24~", "\x1b[2~", "\x1b[3~", "\x1b[7~", "\x1b[8~", "\x1b[5~", "\x1b[6~", "\x1b[A", "\x1b[B", "\x1b[D", "\x1b[C", +} +var eterm_funcs = []string{ + "\x1b7\x1b[?47h", "\x1b[2J\x1b[?47l\x1b8", "\x1b[?25h", "\x1b[?25l", "\x1b[H\x1b[2J", "\x1b[m\x0f", "\x1b[4m", "\x1b[1m", "\x1b[5m", "\x1b[7m", "", "", "", "", +} + +// screen +var screen_keys = []string{ + "\x1bOP", "\x1bOQ", "\x1bOR", "\x1bOS", "\x1b[15~", "\x1b[17~", "\x1b[18~", "\x1b[19~", "\x1b[20~", "\x1b[21~", "\x1b[23~", "\x1b[24~", "\x1b[2~", "\x1b[3~", "\x1b[1~", "\x1b[4~", "\x1b[5~", "\x1b[6~", "\x1bOA", "\x1bOB", "\x1bOD", "\x1bOC", +} +var screen_funcs = []string{ + "\x1b[?1049h", "\x1b[?1049l", "\x1b[34h\x1b[?25h", "\x1b[?25l", "\x1b[H\x1b[J", "\x1b[m\x0f", "\x1b[4m", "\x1b[1m", "\x1b[5m", "\x1b[7m", "\x1b[?1h\x1b=", "\x1b[?1l\x1b>", "\x1b[?1000h", "\x1b[?1000l", +} + +// xterm +var xterm_keys = []string{ + "\x1bOP", "\x1bOQ", "\x1bOR", "\x1bOS", "\x1b[15~", "\x1b[17~", "\x1b[18~", "\x1b[19~", "\x1b[20~", "\x1b[21~", "\x1b[23~", "\x1b[24~", "\x1b[2~", "\x1b[3~", "\x1bOH", "\x1bOF", "\x1b[5~", "\x1b[6~", "\x1bOA", "\x1bOB", "\x1bOD", "\x1bOC", +} +var xterm_funcs = []string{ + "\x1b[?1049h", "\x1b[?1049l", "\x1b[?12l\x1b[?25h", "\x1b[?25l", "\x1b[H\x1b[2J", "\x1b(B\x1b[m", "\x1b[4m", "\x1b[1m", "\x1b[5m", "\x1b[7m", "\x1b[?1h\x1b=", "\x1b[?1l\x1b>", "\x1b[?1000h", "\x1b[?1000l", +} + +// rxvt-unicode +var rxvt_unicode_keys = []string{ + "\x1b[11~", "\x1b[12~", "\x1b[13~", "\x1b[14~", "\x1b[15~", "\x1b[17~", "\x1b[18~", "\x1b[19~", "\x1b[20~", "\x1b[21~", "\x1b[23~", "\x1b[24~", "\x1b[2~", "\x1b[3~", "\x1b[7~", "\x1b[8~", "\x1b[5~", "\x1b[6~", "\x1b[A", "\x1b[B", "\x1b[D", "\x1b[C", +} +var rxvt_unicode_funcs = []string{ + "\x1b[?1049h", "\x1b[r\x1b[?1049l", "\x1b[?25h", "\x1b[?25l", "\x1b[H\x1b[2J", "\x1b[m\x1b(B", "\x1b[4m", "\x1b[1m", "\x1b[5m", "\x1b[7m", "\x1b=", "\x1b>", "\x1b[?1000h", "\x1b[?1000l", +} + +// linux +var linux_keys = []string{ + "\x1b[[A", "\x1b[[B", "\x1b[[C", "\x1b[[D", "\x1b[[E", "\x1b[17~", "\x1b[18~", "\x1b[19~", "\x1b[20~", "\x1b[21~", "\x1b[23~", "\x1b[24~", "\x1b[2~", "\x1b[3~", "\x1b[1~", "\x1b[4~", "\x1b[5~", "\x1b[6~", "\x1b[A", "\x1b[B", "\x1b[D", "\x1b[C", +} +var linux_funcs = []string{ + "", "", "\x1b[?25h\x1b[?0c", "\x1b[?25l\x1b[?1c", "\x1b[H\x1b[J", "\x1b[0;10m", "\x1b[4m", "\x1b[1m", "\x1b[5m", "\x1b[7m", "", "", "", "", +} + +// rxvt-256color +var rxvt_256color_keys = []string{ + "\x1b[11~", "\x1b[12~", "\x1b[13~", "\x1b[14~", "\x1b[15~", "\x1b[17~", "\x1b[18~", "\x1b[19~", "\x1b[20~", "\x1b[21~", "\x1b[23~", "\x1b[24~", "\x1b[2~", "\x1b[3~", "\x1b[7~", "\x1b[8~", "\x1b[5~", "\x1b[6~", "\x1b[A", "\x1b[B", "\x1b[D", "\x1b[C", +} +var rxvt_256color_funcs = []string{ + "\x1b7\x1b[?47h", "\x1b[2J\x1b[?47l\x1b8", "\x1b[?25h", "\x1b[?25l", "\x1b[H\x1b[2J", "\x1b[m\x0f", "\x1b[4m", "\x1b[1m", "\x1b[5m", "\x1b[7m", "\x1b=", "\x1b>", "\x1b[?1000h", "\x1b[?1000l", +} + +var terms = []struct { + name string + keys []string + funcs []string +}{ + {"Eterm", eterm_keys, eterm_funcs}, + {"screen", screen_keys, screen_funcs}, + {"xterm", xterm_keys, xterm_funcs}, + {"rxvt-unicode", rxvt_unicode_keys, rxvt_unicode_funcs}, + {"linux", linux_keys, linux_funcs}, + {"rxvt-256color", rxvt_256color_keys, rxvt_256color_funcs}, +} diff --git a/Godeps/_workspace/src/github.com/pivotal-golang/bytefmt/LICENSE b/Godeps/_workspace/src/github.com/pivotal-golang/bytefmt/LICENSE new file mode 100644 index 0000000..5c304d1 --- /dev/null +++ b/Godeps/_workspace/src/github.com/pivotal-golang/bytefmt/LICENSE @@ -0,0 +1,201 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Godeps/_workspace/src/github.com/pivotal-golang/bytefmt/README.md b/Godeps/_workspace/src/github.com/pivotal-golang/bytefmt/README.md new file mode 100644 index 0000000..7f98e95 --- /dev/null +++ b/Godeps/_workspace/src/github.com/pivotal-golang/bytefmt/README.md @@ -0,0 +1,13 @@ +bytefmt +======= + +Human readable byte formatter + +Example: + +```go + bytefmt.ByteSize(100.5*bytefmt.MEGABYTE) // returns "100.5M" + bytefmt.ByteSize(uint64(1024)) // returns "1K" +``` + +For documentation, please see http://godoc.org/github.com/pivotal-golang/bytefmt diff --git a/Godeps/_workspace/src/github.com/pivotal-golang/bytefmt/bytes.go b/Godeps/_workspace/src/github.com/pivotal-golang/bytefmt/bytes.go new file mode 100644 index 0000000..aa9ef23 --- /dev/null +++ b/Godeps/_workspace/src/github.com/pivotal-golang/bytefmt/bytes.go @@ -0,0 +1,90 @@ +// bytefmt contains helper methods and constants for converting to and from a human readable byte format. +// +// bytefmt.ByteSize(100.5*bytefmt.MEGABYE) // "100.5M" +// bytefmt.ByteSize(uint64(1024)) // "1K" +// +package bytefmt + +import ( + "errors" + "fmt" + "regexp" + "strconv" + "strings" +) + +const ( + BYTE = 1.0 + KILOBYTE = 1024 * BYTE + MEGABYTE = 1024 * KILOBYTE + GIGABYTE = 1024 * MEGABYTE + TERABYTE = 1024 * GIGABYTE +) + +var bytesPattern *regexp.Regexp = regexp.MustCompile(`(?i)^(-?\d+)([KMGT]B?|B)$`) + +var invalidByteQuantityError = errors.New("Byte quantity must be a positive integer with a unit of measurement like M, MB, G, or GB") + +// ByteSize returns a human readable byte string, of the format 10M, 12.5K, etc. The following units are available: +// T Terabyte +// G Gigabyte +// M Megabyte +// K Kilobyte +// the unit that would result in printing the smallest whole number is always chosen +func ByteSize(bytes uint64) string { + unit := "" + value := float32(bytes) + + switch { + case bytes >= TERABYTE: + unit = "T" + value = value / TERABYTE + case bytes >= GIGABYTE: + unit = "G" + value = value / GIGABYTE + case bytes >= MEGABYTE: + unit = "M" + value = value / MEGABYTE + case bytes >= KILOBYTE: + unit = "K" + value = value / KILOBYTE + case bytes >= BYTE: + unit = "B" + case bytes == 0: + return "0" + } + + stringValue := fmt.Sprintf("%.1f", value) + stringValue = strings.TrimSuffix(stringValue, ".0") + return fmt.Sprintf("%s%s", stringValue, unit) +} + +// ToMegabyte parses a string formatted by ByteSize as megabytes +func ToMegabytes(s string) (uint64, error) { + parts := bytesPattern.FindStringSubmatch(strings.TrimSpace(s)) + if len(parts) < 3 { + return 0, invalidByteQuantityError + } + + value, err := strconv.ParseUint(parts[1], 10, 0) + if err != nil || value < 1 { + return 0, invalidByteQuantityError + } + + var bytes uint64 + unit := strings.ToUpper(parts[2]) + switch unit[:1] { + case "T": + bytes = value * TERABYTE + case "G": + bytes = value * GIGABYTE + case "M": + bytes = value * MEGABYTE + case "K": + bytes = value * KILOBYTE + case "B": + bytes = value * BYTE + } + + return bytes / MEGABYTE, nil +} diff --git a/Godeps/_workspace/src/github.com/pivotal-golang/bytefmt/bytes_test.go b/Godeps/_workspace/src/github.com/pivotal-golang/bytefmt/bytes_test.go new file mode 100644 index 0000000..ab87fa0 --- /dev/null +++ b/Godeps/_workspace/src/github.com/pivotal-golang/bytefmt/bytes_test.go @@ -0,0 +1,121 @@ +package bytefmt_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + . "github.com/pivotal-golang/bytefmt" +) + +var _ = Describe("bytefmt", func() { + + Context("ByteSize", func() { + It("Prints in the largest possible unit", func() { + Expect(ByteSize(10 * TERABYTE)).To(Equal("10T")) + Expect(ByteSize(uint64(10.5 * TERABYTE))).To(Equal("10.5T")) + + Expect(ByteSize(10 * GIGABYTE)).To(Equal("10G")) + Expect(ByteSize(uint64(10.5 * GIGABYTE))).To(Equal("10.5G")) + + Expect(ByteSize(100 * MEGABYTE)).To(Equal("100M")) + Expect(ByteSize(uint64(100.5 * MEGABYTE))).To(Equal("100.5M")) + + Expect(ByteSize(100 * KILOBYTE)).To(Equal("100K")) + Expect(ByteSize(uint64(100.5 * KILOBYTE))).To(Equal("100.5K")) + + Expect(ByteSize(1)).To(Equal("1B")) + }) + + It("prints '0' for zero bytes", func() { + Expect(ByteSize(0)).To(Equal("0")) + }) + }) + + Context("ToMegabytes", func() { + It("parses byte amounts with short units (e.g. M, G)", func() { + var ( + megabytes uint64 + err error + ) + + megabytes, err = ToMegabytes("5B") + Expect(megabytes).To(Equal(uint64(0))) + Expect(err).NotTo(HaveOccurred()) + + megabytes, err = ToMegabytes("5K") + Expect(megabytes).To(Equal(uint64(0))) + Expect(err).NotTo(HaveOccurred()) + + megabytes, err = ToMegabytes("5M") + Expect(megabytes).To(Equal(uint64(5))) + Expect(err).NotTo(HaveOccurred()) + + megabytes, err = ToMegabytes("5m") + Expect(megabytes).To(Equal(uint64(5))) + Expect(err).NotTo(HaveOccurred()) + + megabytes, err = ToMegabytes("2G") + Expect(megabytes).To(Equal(uint64(2 * 1024))) + Expect(err).NotTo(HaveOccurred()) + + megabytes, err = ToMegabytes("3T") + Expect(megabytes).To(Equal(uint64(3 * 1024 * 1024))) + Expect(err).NotTo(HaveOccurred()) + }) + + It("parses byte amounts with long units (e.g MB, GB)", func() { + var ( + megabytes uint64 + err error + ) + + megabytes, err = ToMegabytes("5MB") + Expect(megabytes).To(Equal(uint64(5))) + Expect(err).NotTo(HaveOccurred()) + + megabytes, err = ToMegabytes("5mb") + Expect(megabytes).To(Equal(uint64(5))) + Expect(err).NotTo(HaveOccurred()) + + megabytes, err = ToMegabytes("2GB") + Expect(megabytes).To(Equal(uint64(2 * 1024))) + Expect(err).NotTo(HaveOccurred()) + + megabytes, err = ToMegabytes("3TB") + Expect(megabytes).To(Equal(uint64(3 * 1024 * 1024))) + Expect(err).NotTo(HaveOccurred()) + }) + + It("returns an error when the unit is missing", func() { + _, err := ToMegabytes("5") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("unit of measurement")) + }) + + It("returns an error when the unit is unrecognized", func() { + _, err := ToMegabytes("5MBB") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("unit of measurement")) + + _, err = ToMegabytes("5BB") + Expect(err).To(HaveOccurred()) + }) + + It("allows whitespace before and after the value", func() { + megabytes, err := ToMegabytes("\t\n\r 5MB ") + Expect(megabytes).To(Equal(uint64(5))) + Expect(err).NotTo(HaveOccurred()) + }) + + It("returns an error for negative values", func() { + _, err := ToMegabytes("-5MB") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("unit of measurement")) + }) + + It("returns an error for zero values", func() { + _, err := ToMegabytes("0TB") + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("unit of measurement")) + }) + }) +}) diff --git a/Godeps/_workspace/src/github.com/pivotal-golang/bytefmt/formatters_suite_test.go b/Godeps/_workspace/src/github.com/pivotal-golang/bytefmt/formatters_suite_test.go new file mode 100644 index 0000000..64af7ca --- /dev/null +++ b/Godeps/_workspace/src/github.com/pivotal-golang/bytefmt/formatters_suite_test.go @@ -0,0 +1,13 @@ +package bytefmt_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestFormatters(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Bytefmt Suite") +} diff --git a/LICENSE b/LICENSE index 8f71f43..72c7662 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright {yyyy} {name of copyright owner} + Copyright 2015 Swisscom (Schweiz) AG Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 70b422e..fb34efb 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,45 @@ -# cf-statistics-plugin +# Cloud Foundry CLI Statistics Plugin CloudFoundry CLI plugin for displaying real-time metrics and statistics data + +This plugin displays a terminal dashboard showing current app usage metrics/statistics. +It can also stream these metrics as JSON formatted output to stdout. + +[![cf-statistics-plugin](screencast.gif)](screencast.webm) + +## Installation + +#### Install from CLI (v.6.10.0 and up) +``` +$ cf add-plugin-repo CF-Community http://plugins.cloudfoundry.org/ +$ cf install-plugin Statistics -r CF-Community +``` + +#### Install from binary +- Download the appropriate plugin binary from [releases](https://github.com/swisscom/cf-statistics-plugin/releases) +- Install the plugin: `$ cf install-plugin ` + +#### Install from Source +``` +$ go get github.com/swisscom/cf-statistics-plugin +$ cf install-plugin $GOPATH/bin/cf-statistics-plugin +``` + +## Usage + +``` +$ cf statistics APP_NAME +``` + +## Uninstall + +``` +$ cf uninstall-plugin Statistics +``` + +## Commands + +| command/option | usage | description| +| :--------------- |:---------------| :------------| +|`statistics`| `cf statistics APP_NAME` |display live metrics/statistics dashboard| +|`--debug`|`cf statistics APP_NAME --debug`|prints metrics to stdout in JSON format| +|`--full`|`cf statistics APP_NAME --debug --full`|prints full statistics to stdout| diff --git a/helper/command.go b/helper/command.go new file mode 100644 index 0000000..4154d21 --- /dev/null +++ b/helper/command.go @@ -0,0 +1,35 @@ +/* + Copyright 2015 Swisscom (Schweiz) AG + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package helper + +import ( + "bytes" + "fmt" + "os/exec" +) + +func CallCommandHelp(command string, errorText string) { + var out bytes.Buffer + + cmd := exec.Command("cf", command, "-h") + cmd.Stdout = &out + cmd.Run() + + fmt.Println("\nFAILED") + fmt.Println(errorText) + fmt.Println("") + fmt.Println(out.String()) +} diff --git a/screencast.gif b/screencast.gif new file mode 100644 index 0000000..4b3645f Binary files /dev/null and b/screencast.gif differ diff --git a/screencast.webm b/screencast.webm new file mode 100644 index 0000000..cfc76dd Binary files /dev/null and b/screencast.webm differ diff --git a/statistics-plugin.go b/statistics-plugin.go new file mode 100644 index 0000000..f09c18e --- /dev/null +++ b/statistics-plugin.go @@ -0,0 +1,59 @@ +/* + Copyright 2015 Swisscom (Schweiz) AG + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package main + +import ( + "github.com/cloudfoundry/cli/plugin" + "github.com/swisscom/cf-statistics-plugin/statistics" +) + +type StatisticsPlugin struct{} + +func main() { + plugin.Start(&StatisticsPlugin{}) +} + +func (s *StatisticsPlugin) GetMetadata() plugin.PluginMetadata { + return plugin.PluginMetadata{ + Name: "Statistics", + Version: plugin.VersionType{ + Major: 1, + Minor: 0, + Build: 0, + }, + Commands: []plugin.Command{ + { + Name: "statistics", + Alias: "stats", + HelpText: "display live metrics/statistics about an app", + UsageDetails: plugin.Usage{ + Usage: "cf statistics APP_NAME [--debug] [--full]", + Options: map[string]string{ + "debug": "Prints metrics to stdout in JSON format", + "full": "Prints full statistics to stdout if --debug is enabled", + }, + }, + }, + }, + } +} + +func (s *StatisticsPlugin) Run(cliConnection plugin.CliConnection, args []string) { + switch args[0] { + case "statistics", "stats": + statistics.Run(cliConnection, args[1:]) + } +} diff --git a/statistics/plugin_hook.go b/statistics/plugin_hook.go new file mode 100644 index 0000000..3eef0fc --- /dev/null +++ b/statistics/plugin_hook.go @@ -0,0 +1,197 @@ +/* + Copyright 2015 Swisscom (Schweiz) AG + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package statistics + +import ( + "encoding/json" + "fmt" + "os" + "strings" + "sync" + "time" + + "github.com/cloudfoundry/cli/cf/configuration/config_helpers" + "github.com/cloudfoundry/cli/cf/configuration/core_config" + "github.com/cloudfoundry/cli/plugin" + ui "github.com/gizak/termui" + "github.com/swisscom/cf-statistics-plugin/helper" +) + +var ( + commandLock = &sync.Mutex{} +) + +type Search struct { + Resources []struct { + Metadata struct { + Guid string `json:"guid"` + } `json:"metadata"` + } `json:"resources"` +} + +func Run(cliConnection plugin.CliConnection, args []string) { + if len(args) == 0 { + helper.CallCommandHelp("statistics", "Incorrect usage.\n") + os.Exit(1) + } + + var debugOutput, fullOutput bool + for _, arg := range args[1:] { + switch arg { + case "--debug": + debugOutput = true + case "--full": + fullOutput = true + } + } + + appName := args[0] + appGuid := getAppGuid(cliConnection, appName) + + statsChan := make(chan Statistics) + errChan := make(chan error) + defer close(statsChan) + defer close(errChan) + + go pollStats(cliConnection, appGuid, statsChan, errChan) + + // setup ui + if !debugOutput { + if err := newTerminalUI(appName); err != nil { + fmt.Println("\nERROR:", err) + os.Exit(1) + } + defer term.Close() + } + + // main loop + var nofInstances int + for { + select { + + // ui events + case e := <-ui.EventCh(): + if e.Type == ui.EventKey { + if e.Ch == 'q' || e.Key == ui.KeyCtrlC { + term.Close() + os.Exit(0) + } else if e.Key == ui.KeyPgup { + // ui shows max 8 instances + if nofInstances < 8 { + scaleApp(cliConnection, appName, nofInstances+1) + } + } else if e.Key == ui.KeyPgdn { + if nofInstances > 1 { + scaleApp(cliConnection, appName, nofInstances-1) + } + } + } + + if e.Type == ui.EventResize { + term.Resize() + } + + if e.Type == ui.EventError { + term.Close() + fmt.Println("\nERROR:", e.Err) + os.Exit(0) + } + + case stats := <-statsChan: + nofInstances = len(stats.Instances) + + // print to stdout if --debug is set + if debugOutput { + for _, idx := range stats.Instances { + var data interface{} + + // print only usage metrics if --full is not set + data = stats.Data[idx].Stats.Usage + if fullOutput { + data = stats.Data[idx] + } + + output, err := json.Marshal(data) + if err != nil { + fmt.Println("\nERROR:", err) + os.Exit(1) + } + fmt.Printf("{\"instance_index\":\"%s\",\"metrics\":%v}\n", idx, string(output)) + } + } else { + // render terminal dashboard + term.UpdateStatistics(stats) + } + + case err := <-errChan: + if !debugOutput { + term.Close() + } + fmt.Println("\nERROR:", err) + os.Exit(1) + + case <-time.After(time.Second * 15): + if !debugOutput { + term.Close() + } + fmt.Println("\nTIMEOUT") + fmt.Println("Querying metrics took too long.. Check your connectivity!") + os.Exit(1) + } + } +} + +func getAppGuid(cliConnection plugin.CliConnection, appName string) string { + repo := core_config.NewRepositoryFromFilepath(config_helpers.DefaultFilePath(), func(err error) { + if err != nil { + fmt.Println("\nERROR:", err) + os.Exit(1) + } + }) + spaceGuid := repo.SpaceFields().Guid + + cmd := []string{"curl", fmt.Sprintf("/v2/spaces/%v/apps?q=name:%v&inline-relations-depth=1", spaceGuid, appName)} + output, err := cliConnection.CliCommandWithoutTerminalOutput(cmd...) + if err != nil { + for _, e := range output { + fmt.Println(e) + } + os.Exit(1) + } + + search := &Search{} + if err := json.Unmarshal([]byte(strings.Join(output, "")), &search); err != nil { + fmt.Println("\nERROR:", err) + os.Exit(1) + } + + return search.Resources[0].Metadata.Guid +} + +func scaleApp(cliConnection plugin.CliConnection, appName string, instances int) { + // lock mutex, to avoid colliding with other cli commands + commandLock.Lock() + defer commandLock.Unlock() + + term.ScaleApp(appName, instances) + + cmd := []string{"scale", appName, "-i", fmt.Sprintf("%d", instances)} + if _, err := cliConnection.CliCommandWithoutTerminalOutput(cmd...); err != nil { + term.Close() + fmt.Println("\nERROR:", err) + os.Exit(0) + } +} diff --git a/statistics/poll.go b/statistics/poll.go new file mode 100644 index 0000000..b7db283 --- /dev/null +++ b/statistics/poll.go @@ -0,0 +1,87 @@ +/* + Copyright 2015 Swisscom (Schweiz) AG + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package statistics + +import ( + "encoding/json" + "fmt" + "sort" + "strings" + "time" + + "github.com/cloudfoundry/cli/plugin" +) + +type Statistics struct { + Instances []string + Data map[string]Instance +} +type Instance struct { + State string `json:"state"` + Stats struct { + Name string `json:"name"` + URIs []string `json:"uris"` + Host string `json:"host"` + Port int `json:"port"` + Uptime int64 `json:"uptime"` + MemoryQuota int64 `json:"mem_quota"` + DiskQuota int64 `json:"disk_quota"` + FiledescriptorQuota int `json:"fds_quota"` + Usage struct { + Time string `json:"time"` + CPU float64 `json:"cpu"` + Memory int64 `json:"mem"` + Disk int64 `json:"disk"` + } `json:"usage"` + } `json:"stats"` +} + +func pollStats(cliConnection plugin.CliConnection, appGuid string, outChan chan<- Statistics, errChan chan<- error) { + cmd := []string{"curl", fmt.Sprintf("/v2/apps/%v/stats", appGuid)} + + for { + // lock mutex, to avoid colliding with possible other cli commands, like for example app scaling + commandLock.Lock() + output, err := cliConnection.CliCommandWithoutTerminalOutput(cmd[:]...) + commandLock.Unlock() + if err != nil { + for _, e := range output { + fmt.Println(e) + } + errChan <- err + break + } + + var stats Statistics + if err := json.Unmarshal([]byte(strings.Join(output, "")), &stats.Data); err != nil { + errChan <- err + break + } + + // get instance_index list & sort it + var instances []string + for key, _ := range stats.Data { + instances = append(instances, key) + } + sort.StringSlice(instances).Sort() + stats.Instances = instances + + // send statistics back + outChan <- stats + + time.Sleep(time.Second * 1) + } +} diff --git a/statistics/terminal.go b/statistics/terminal.go new file mode 100644 index 0000000..1b7a48e --- /dev/null +++ b/statistics/terminal.go @@ -0,0 +1,307 @@ +/* + Copyright 2015 Swisscom (Schweiz) AG + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ +package statistics + +import ( + "fmt" + "math" + "time" + + ui "github.com/gizak/termui" + "github.com/pivotal-golang/bytefmt" +) + +var ( + term *TerminalUI + colors = []ui.Attribute{ + ui.ColorGreen, + ui.ColorMagenta, + ui.ColorRed, + ui.ColorBlue, + ui.ColorYellow, + ui.ColorCyan, + } +) + +type TerminalUI struct { + Body *ui.Grid + Usage *ui.Par + Summary *ui.Par + CPU *ui.Sparklines + Memory []*ui.Gauge + Disk *ui.BarChart +} + +func newTerminalUI(appName string) error { + if err := ui.Init(); err != nil { + return err + } + ui.UseTheme("helloworld") + + // usage text + usageText := fmt.Sprintf(`Show live statistics for [%s] + +:Press 'q' or 'ctrl-c' to exit +:Press 'PageUp' to increase app instances +:Press 'PageDown' to decrease app instances`, appName) + usage := ui.NewPar(usageText) + usage.Height = 12 + usage.TextFgColor = ui.ColorWhite + usage.Border.Label = "Usage" + usage.Border.FgColor = ui.ColorCyan + + // summary text + summary := ui.NewPar("") + summary.Height = 12 + summary.TextFgColor = ui.ColorRed + summary.Border.Label = "Summary" + summary.Border.FgColor = ui.ColorCyan + + // cpu sparklines + data := [400]int{} + line := ui.NewSparkline() + line.Data = data[:] + line.Height = 4 + line.LineColor = colors[0] + cpu := ui.NewSparklines(line) + cpu.Height = 7 + cpu.Border.Label = "CPU Usage" + + // memory gauges + mem := make([]*ui.Gauge, 1) + for i := range mem { + mem[i] = ui.NewGauge() + mem[i].Percent = 0 + mem[i].Height = 5 + } + + // disk bars + disk := ui.NewBarChart() + disk.Border.Label = "Disk Usage (in MB)" + disk.Data = []int{0, 0, 0, 0, 0, 0, 0, 0} + disk.Height = 12 + disk.BarWidth = 10 + disk.DataLabels = []string{"I: 0", "I: 1", "I: 2", "I: 3", "I: 4", "I: 5", "I: 6", "I: 7"} + disk.TextColor = ui.ColorWhite + disk.BarColor = ui.ColorYellow + disk.NumColor = ui.ColorWhite + + term = &TerminalUI{ui.Body, usage, summary, cpu, mem, disk} + return nil +} + +func (t *TerminalUI) Close() { + ui.Close() +} + +func (t *TerminalUI) Resize() { + ui.Body.Width = ui.TermWidth() + ui.Body.Align() + ui.Render(ui.Body) +} + +func (t *TerminalUI) Render() { + t.Resize() +} + +func (t *TerminalUI) AdjustCPU(stats Statistics) { + // adjust number of sparklines to number of app instances + if len(stats.Instances) < len(t.CPU.Lines) { + t.CPU.Lines = t.CPU.Lines[:len(stats.Instances)] + } + if len(stats.Instances) > len(t.CPU.Lines) { + for i := len(t.CPU.Lines); i < len(stats.Instances); i++ { + // show max 8 instances + if len(t.CPU.Lines) > 7 { + break + } + + // new sparkline + data := [400]int{} + line := ui.NewSparkline() + line.Data = data[:] + line.LineColor = colors[i%6] + + // add sparkline + t.CPU.Lines = append(t.CPU.Lines, line) + } + } + t.CPU.Height = len(t.CPU.Lines)*(13-min(len(stats.Instances), 8)) + 2 + + // calculate and update data values + for i, idx := range stats.Instances { + // show max 8 instances + if i > 7 { + break + } + cpu := int(math.Ceil(stats.Data[idx].Stats.Usage.CPU * 100 * 100 * 100)) + t.CPU.Lines[i].Data = append(t.CPU.Lines[i].Data[1:], cpu) + t.CPU.Lines[i].Title = fmt.Sprintf("Instance %d: %.2f%%", i, stats.Data[idx].Stats.Usage.CPU*100.0) + t.CPU.Lines[i].Height = 12 - min(len(stats.Instances), 8) + } +} + +func (t *TerminalUI) AdjustMemory(stats Statistics) { + // memory gauges + mem := make([]*ui.Gauge, len(stats.Instances)) + for i, idx := range stats.Instances { + // show max 8 instances + if i > 7 { + break + } + + memory := uint64(stats.Data[idx].Stats.Usage.Memory) + quota := uint64(stats.Data[idx].Stats.MemoryQuota) + percent := int(math.Ceil((float64(memory) / float64(quota)) * 100.0)) + mem[i] = ui.NewGauge() + mem[i].Percent = percent + mem[i].Height = 13 - min(len(stats.Instances), 8) + mem[i].Border.Label = fmt.Sprintf("Memory - Instance %d: %d%% (%s / %s)", + i, percent, bytefmt.ByteSize(memory), bytefmt.ByteSize(quota)) + mem[i].Border.FgColor = ui.ColorWhite + mem[i].Border.LabelFgColor = ui.ColorWhite + mem[i].BarColor = colors[i%6] + mem[i].PercentColor = ui.ColorWhite + } + t.Memory = mem + + // update layout + ui.Body.Rows = []*ui.Row{ + ui.NewRow( + ui.NewCol(3, 0, t.Usage), + ui.NewCol(3, 0, t.Summary), + ui.NewCol(6, 0, t.Disk)), + ui.NewRow( + ui.NewCol(6, 0, t.CPU), + t.newMemCol(6, 0, t.Memory)), + } +} + +func (t *TerminalUI) AdjustDisk(stats Statistics) { + var quota int64 + var data []int + for i, idx := range stats.Instances { + // show max 8 instances + if i > 7 { + break + } + + mb := int((stats.Data[idx].Stats.Usage.Disk / 1024) / 1024) + data = append(data, mb) + + quota = stats.Data[idx].Stats.DiskQuota + } + t.Disk.Data = data + t.Disk.BarWidth = 20 - min(len(stats.Instances), 8) + t.Disk.Border.Label = fmt.Sprintf("Disk Usage (in MB) - Quota: %s", bytefmt.ByteSize(uint64(quota))) +} + +func (t *TerminalUI) AdjustSummary(stats Statistics) { + var uptime int64 + var cpuUsage float64 + var memUsage, memQuota, diskUsage, diskQuota int64 + for _, idx := range stats.Instances { + uptime = max(uptime, stats.Data[idx].Stats.Uptime) + cpuUsage += stats.Data[idx].Stats.Usage.CPU + memUsage += stats.Data[idx].Stats.Usage.Memory + diskUsage += stats.Data[idx].Stats.Usage.Disk + memQuota += stats.Data[idx].Stats.MemoryQuota + diskQuota += stats.Data[idx].Stats.DiskQuota + } + up, _ := time.ParseDuration(fmt.Sprintf("%ds", uptime)) + + summaryText := fmt.Sprintf( + `Instances running: %d + +Uptime: %s +Up Since: %s + +Total CPU Usage: %.4f%% +Total Memory Usage: %s / %s +Total Disk Usage: %s / %s`, + len(stats.Instances), + up.String(), + time.Now().Add(-1*up).Format("Mon, 02 Jan 2006 15:04 MST"), + cpuUsage*100.0, + bytefmt.ByteSize(uint64(memUsage)), bytefmt.ByteSize(uint64(memQuota)), + bytefmt.ByteSize(uint64(diskUsage)), bytefmt.ByteSize(uint64(diskQuota))) + t.Summary.Text = summaryText +} + +func (t *TerminalUI) newMemCol(span, offset int, mem []*ui.Gauge) *ui.Row { + // soooo ugly.. :( + switch len(mem) { + case 0: + return ui.NewCol(span, offset, nil) + case 1: + return ui.NewCol(span, offset, mem[0]) + case 2: + return ui.NewCol(span, offset, mem[0], mem[1]) + case 3: + return ui.NewCol(span, offset, mem[0], mem[1], mem[2]) + case 4: + return ui.NewCol(span, offset, mem[0], mem[1], mem[2], mem[3]) + case 5: + return ui.NewCol(span, offset, mem[0], mem[1], mem[2], mem[3], mem[4]) + case 6: + return ui.NewCol(span, offset, mem[0], mem[1], mem[2], mem[3], mem[4], mem[5]) + case 7: + return ui.NewCol(span, offset, mem[0], mem[1], mem[2], mem[3], mem[4], mem[5], mem[6]) + default: + return ui.NewCol(span, offset, mem[0], mem[1], mem[2], mem[3], mem[4], mem[5], mem[6], mem[7]) + } + return nil +} + +func (t *TerminalUI) ScaleApp(appName string, instances int) { + // scaling text + scaling := ui.NewPar(fmt.Sprintf("\nSCALING [%s] TO [%d] INSTANCES...\n", appName, instances)) + scaling.Height = 5 + scaling.TextFgColor = ui.ColorYellow + scaling.Border.Label = "Scale" + scaling.Border.FgColor = ui.ColorRed + scaling.Border.LabelFgColor = ui.ColorWhite + scaling.Border.LabelBgColor = ui.ColorRed + + ui.Body.Rows = []*ui.Row{ui.NewRow( + ui.NewCol(8, 2, scaling), + )} + + term.Render() +} + +func (t *TerminalUI) UpdateStatistics(stats Statistics) { + t.AdjustCPU(stats) + t.AdjustMemory(stats) + t.AdjustDisk(stats) + t.AdjustSummary(stats) + + t.Render() +} + +func min(a, b int) int { + if a > b { + return b + } + return a +} + +func max(a, b int64) int64 { + if a > b { + return a + } + return b +} diff --git a/update_plugin.sh b/update_plugin.sh new file mode 100755 index 0000000..3f23e5c --- /dev/null +++ b/update_plugin.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +cf uninstall-plugin Statistics +rm -f $GOPATH/bin/cf-statistics-plugin + +set -e +go build +go install + +cf install-plugin $GOPATH/bin/cf-statistics-plugin +cf plugins