| @ -0,0 +1,117 @@ | |||
| # Created by https://www.toptal.com/developers/gitignore/api/goland+all,go | |||
| # Edit at https://www.toptal.com/developers/gitignore?templates=goland+all,go | |||
| ### Go ### | |||
| # If you prefer the allow list template instead of the deny list, see community template: | |||
| # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore | |||
| # | |||
| # Binaries for programs and plugins | |||
| *.exe | |||
| *.exe~ | |||
| *.dll | |||
| *.so | |||
| *.dylib | |||
| # Test binary, built with `go test -c` | |||
| *.test | |||
| # Output of the go coverage tool, specifically when used with LiteIDE | |||
| *.out | |||
| # Dependency directories (remove the comment below to include it) | |||
| # vendor/ | |||
| # Go workspace file | |||
| go.work | |||
| ### GoLand+all ### | |||
| # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider | |||
| # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 | |||
| # User-specific stuff | |||
| .idea/**/workspace.xml | |||
| .idea/**/tasks.xml | |||
| .idea/**/usage.statistics.xml | |||
| .idea/**/dictionaries | |||
| .idea/**/shelf | |||
| # AWS User-specific | |||
| .idea/**/aws.xml | |||
| # Generated files | |||
| .idea/**/contentModel.xml | |||
| # Sensitive or high-churn files | |||
| .idea/**/dataSources/ | |||
| .idea/**/dataSources.ids | |||
| .idea/**/dataSources.local.xml | |||
| .idea/**/sqlDataSources.xml | |||
| .idea/**/dynamic.xml | |||
| .idea/**/uiDesigner.xml | |||
| .idea/**/dbnavigator.xml | |||
| # Gradle | |||
| .idea/**/gradle.xml | |||
| .idea/**/libraries | |||
| # Gradle and Maven with auto-import | |||
| # When using Gradle or Maven with auto-import, you should exclude module files, | |||
| # since they will be recreated, and may cause churn. Uncomment if using | |||
| # auto-import. | |||
| # .idea/artifacts | |||
| # .idea/compiler.xml | |||
| # .idea/jarRepositories.xml | |||
| # .idea/modules.xml | |||
| # .idea/*.iml | |||
| # .idea/modules | |||
| # *.iml | |||
| # *.ipr | |||
| # CMake | |||
| cmake-build-*/ | |||
| # Mongo Explorer plugin | |||
| .idea/**/mongoSettings.xml | |||
| # File-based project format | |||
| *.iws | |||
| # IntelliJ | |||
| out/ | |||
| # mpeltonen/sbt-idea plugin | |||
| .idea_modules/ | |||
| # JIRA plugin | |||
| atlassian-ide-plugin.xml | |||
| # Cursive Clojure plugin | |||
| .idea/replstate.xml | |||
| # SonarLint plugin | |||
| .idea/sonarlint/ | |||
| # Crashlytics plugin (for Android Studio and IntelliJ) | |||
| com_crashlytics_export_strings.xml | |||
| crashlytics.properties | |||
| crashlytics-build.properties | |||
| fabric.properties | |||
| # Editor-based Rest Client | |||
| .idea/httpRequests | |||
| # Android studio 3.1+ serialized cache file | |||
| .idea/caches/build_file_checksums.ser | |||
| ### GoLand+all Patch ### | |||
| # Ignore everything but code style settings and run configurations | |||
| # that are supposed to be shared within teams. | |||
| .idea/* | |||
| !.idea/codeStyles | |||
| !.idea/runConfigurations | |||
| # End of https://www.toptal.com/developers/gitignore/api/goland+all,go | |||
| build | |||
| @ -0,0 +1,17 @@ | |||
| package main | |||
| import ( | |||
| "git.brettb.xyz/goinv/client/internal/app" | |||
| "go.uber.org/zap" | |||
| ) | |||
| func main() { | |||
| logger, _ := zap.NewProduction() | |||
| defer logger.Sync() | |||
| a := app.NewApp("goinv", "0.1.0", "http://localhost:3001", logger) // TODO: CONFIGURATION | |||
| if err := a.Run(); err != nil { | |||
| logger.Panic("application errored", zap.Error(err)) | |||
| } | |||
| } | |||
| @ -0,0 +1,24 @@ | |||
| module git.brettb.xyz/goinv/client | |||
| go 1.21.6 | |||
| require ( | |||
| github.com/gdamore/tcell/v2 v2.6.1-0.20231203215052-2917c3801e73 | |||
| github.com/magefile/mage v1.15.0 | |||
| go.uber.org/zap v1.26.0 | |||
| ) | |||
| require ( | |||
| github.com/gdamore/encoding v1.0.0 // indirect | |||
| github.com/lucasb-eyer/go-colorful v1.2.0 // indirect | |||
| github.com/mattn/go-runewidth v0.0.14 // indirect | |||
| github.com/rivo/uniseg v0.4.3 // indirect | |||
| golang.org/x/sys v0.11.0 // indirect | |||
| golang.org/x/term v0.9.0 // indirect | |||
| golang.org/x/text v0.12.0 // indirect | |||
| ) | |||
| require ( | |||
| github.com/rivo/tview v0.0.0-20240116070845-bf8f1c43e46c | |||
| go.uber.org/multierr v1.10.0 // indirect | |||
| ) | |||
| @ -0,0 +1,66 @@ | |||
| github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= | |||
| github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= | |||
| github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= | |||
| github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= | |||
| github.com/gdamore/tcell/v2 v2.6.1-0.20231203215052-2917c3801e73 h1:SeDV6ZUSVlTAUUPdMzPXgMyj96z+whQJRRUff8dIeic= | |||
| github.com/gdamore/tcell/v2 v2.6.1-0.20231203215052-2917c3801e73/go.mod h1:pwzJMyH4Hd0AZMJkWQ+/g01dDvYWEvmJuaiRU71Xl8k= | |||
| github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= | |||
| github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= | |||
| github.com/magefile/mage v1.15.0 h1:BvGheCMAsG3bWUDbZ8AyXXpCNwU9u5CB6sM+HNb9HYg= | |||
| github.com/magefile/mage v1.15.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A= | |||
| github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= | |||
| github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= | |||
| github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= | |||
| github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= | |||
| github.com/rivo/tview v0.0.0-20240116070845-bf8f1c43e46c h1:l7KIWhypqjpOJzJKynao/m8Ku4Y6PcZDlVcOVirYkKs= | |||
| github.com/rivo/tview v0.0.0-20240116070845-bf8f1c43e46c/go.mod h1:c0SPlNPXkM+/Zgjn/0vD3W0Ds1yxstN7lpquqLDpWCg= | |||
| github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= | |||
| github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw= | |||
| github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= | |||
| github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= | |||
| github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= | |||
| github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= | |||
| go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= | |||
| go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= | |||
| go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ= | |||
| go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= | |||
| go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo= | |||
| go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so= | |||
| golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= | |||
| golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= | |||
| golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= | |||
| golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= | |||
| golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= | |||
| golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= | |||
| golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= | |||
| golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= | |||
| golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | |||
| golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | |||
| golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= | |||
| golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= | |||
| golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= | |||
| golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | |||
| golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | |||
| golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | |||
| golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | |||
| golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | |||
| golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= | |||
| golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= | |||
| golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= | |||
| golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= | |||
| golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= | |||
| golang.org/x/term v0.9.0 h1:GRRCnKYhdQrD8kfRAdQ6Zcw1P0OcELxGLKJvtjVMZ28= | |||
| golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo= | |||
| golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= | |||
| golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= | |||
| golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= | |||
| golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= | |||
| golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= | |||
| golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= | |||
| golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= | |||
| golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= | |||
| golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= | |||
| golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= | |||
| golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= | |||
| gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= | |||
| gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= | |||
| @ -0,0 +1,146 @@ | |||
| package api | |||
| import ( | |||
| "bytes" | |||
| "fmt" | |||
| "io" | |||
| "net/http" | |||
| "git.brettb.xyz/goinv/client/internal/types" | |||
| ) | |||
| const userAgent = "goinv-cli" | |||
| type APIClient struct { | |||
| Host string | |||
| Token string | |||
| } | |||
| func NewAPIClient(host string) *APIClient { | |||
| return &APIClient{ | |||
| Host: host, | |||
| } | |||
| } | |||
| func (c *APIClient) retrieveSingleAsset(r *http.Request) (*types.AssetResponse, error) { | |||
| r.Header.Set("User-Agent", userAgent) | |||
| //r.Header.Set("Authorization", "Bearer "+c.Token) | |||
| r.Header.Set("Accept", "application/json") | |||
| r.Header.Set("Content-Type", "application/json") | |||
| httpCli := http.Client{} | |||
| httpResp, err := httpCli.Do(r) | |||
| if err != nil { | |||
| return nil, err | |||
| } | |||
| defer httpResp.Body.Close() | |||
| body, err := io.ReadAll(httpResp.Body) | |||
| if err != nil { | |||
| return nil, err | |||
| } | |||
| resp, err := unmarshal[types.AssetResponse](body) | |||
| if err != nil { | |||
| return nil, fmt.Errorf("%s", "An unexpected response was received") | |||
| } | |||
| return resp, nil | |||
| } | |||
| func (c *APIClient) retrieveMultipleAssets(r *http.Request) (*types.MultipleAssetsResponse, error) { | |||
| r.Header.Set("User-Agent", userAgent) | |||
| //r.Header.Set("Authorization", "Bearer "+c.Token) | |||
| r.Header.Set("Accept", "application/json") | |||
| r.Header.Set("Content-Type", "application/json") | |||
| httpCli := http.Client{} | |||
| httpResp, err := httpCli.Do(r) | |||
| if err != nil { | |||
| return nil, err | |||
| } | |||
| defer httpResp.Body.Close() | |||
| body, err := io.ReadAll(httpResp.Body) | |||
| if err != nil { | |||
| return nil, err | |||
| } | |||
| resp, err := unmarshal[types.MultipleAssetsResponse](body) | |||
| if err != nil { | |||
| return nil, fmt.Errorf("%s", "An unexpected response was received") | |||
| } | |||
| return resp, nil | |||
| } | |||
| func (c *APIClient) retrieveMultipleShelves(r *http.Request) (*types.MultipleShelfResponse, error) { | |||
| r.Header.Set("User-Agent", userAgent) | |||
| //r.Header.Set("Authorization", "Bearer "+c.Token) | |||
| r.Header.Set("Accept", "application/json") | |||
| r.Header.Set("Content-Type", "application/json") | |||
| httpCli := http.Client{} | |||
| httpResp, err := httpCli.Do(r) | |||
| if err != nil { | |||
| return nil, err | |||
| } | |||
| defer httpResp.Body.Close() | |||
| body, err := io.ReadAll(httpResp.Body) | |||
| if err != nil { | |||
| return nil, err | |||
| } | |||
| resp, err := unmarshal[types.MultipleShelfResponse](body) | |||
| if err != nil { | |||
| return nil, fmt.Errorf("%s", "An unexpected response was received") | |||
| } | |||
| return resp, nil | |||
| } | |||
| func (c *APIClient) RetrieveAssetByID(idNumber string) (*types.Asset, error) { | |||
| url := fmt.Sprintf("%s/assets/%s", c.Host, idNumber) | |||
| req, _ := http.NewRequest("GET", url, bytes.NewBuffer([]byte{})) | |||
| resp, err := c.retrieveSingleAsset(req) | |||
| if err != nil { | |||
| return nil, err | |||
| } | |||
| return resp.Asset, nil | |||
| } | |||
| func (c *APIClient) RetrieveAllAssets() ([]*types.Asset, error) { | |||
| url := fmt.Sprintf("%s/assets", c.Host) | |||
| req, _ := http.NewRequest("GET", url, bytes.NewBuffer([]byte{})) | |||
| resp, err := c.retrieveMultipleAssets(req) | |||
| if err != nil { | |||
| return nil, err | |||
| } | |||
| return resp.Assets, nil | |||
| } | |||
| func (c *APIClient) DeleteAssetByID(idNumber string) (*types.Asset, error) { | |||
| url := fmt.Sprintf("%s/assets/%s", c.Host, idNumber) | |||
| req, _ := http.NewRequest("DELETE", url, bytes.NewBuffer([]byte{})) | |||
| resp, err := c.retrieveSingleAsset(req) | |||
| if err != nil { | |||
| return nil, err | |||
| } | |||
| return resp.Asset, nil | |||
| } | |||
| func (c *APIClient) RetrieveAllShelves() ([]*types.ShelfLocation, error) { | |||
| url := fmt.Sprintf("%s/shelves", c.Host) | |||
| req, _ := http.NewRequest("GET", url, bytes.NewBuffer([]byte{})) | |||
| resp, err := c.retrieveMultipleShelves(req) | |||
| if err != nil { | |||
| return nil, err | |||
| } | |||
| return resp.ShelfLocations, nil | |||
| } | |||
| @ -0,0 +1,11 @@ | |||
| package api | |||
| import ( | |||
| "encoding/json" | |||
| ) | |||
| func unmarshal[T any](data []byte) (*T, error) { | |||
| var r T | |||
| err := json.Unmarshal(data, &r) | |||
| return &r, err | |||
| } | |||
| @ -0,0 +1,112 @@ | |||
| package app | |||
| import ( | |||
| "os" | |||
| "git.brettb.xyz/goinv/client/internal/api" | |||
| "git.brettb.xyz/goinv/client/internal/ui/assets" | |||
| "git.brettb.xyz/goinv/client/internal/ui/help" | |||
| "git.brettb.xyz/goinv/client/internal/ui/utils" | |||
| "github.com/gdamore/tcell/v2" | |||
| "github.com/rivo/tview" | |||
| "go.uber.org/zap" | |||
| ) | |||
| type App struct { | |||
| *tview.Application | |||
| APIClient *api.APIClient | |||
| pages *tview.Pages | |||
| logger *zap.Logger | |||
| help *help.Help | |||
| assets *assets.Assets | |||
| menu *tview.TextView | |||
| currentPage string | |||
| needInitUI bool | |||
| } | |||
| func NewApp(name, version, host string, logger *zap.Logger) *App { | |||
| sugar := logger.Sugar() | |||
| sugar.Debug("creating app") | |||
| app := App{ | |||
| Application: tview.NewApplication(), | |||
| APIClient: api.NewAPIClient(host), | |||
| pages: tview.NewPages(), | |||
| logger: logger, | |||
| needInitUI: false, | |||
| } | |||
| app.assets = assets.NewAssets(logger, app.APIClient) | |||
| app.help = help.NewHelp(name, version) | |||
| menuItems := [][]string{ | |||
| {utils.HelpScreenKey.Label(), app.help.GetTitle()}, | |||
| {utils.AssetsScreenKey.Label(), app.assets.GetTitle()}, | |||
| } | |||
| app.menu = makeMenu(menuItems) | |||
| app.pages.AddPage(app.help.GetTitle(), app.help, true, false) | |||
| app.pages.AddPage(app.assets.GetTitle(), app.assets, true, false) | |||
| return &app | |||
| } | |||
| func (app *App) Run() error { | |||
| app.logger.Info("starting app") | |||
| flex := tview.NewFlex(). | |||
| SetDirection(tview.FlexRow). | |||
| AddItem(app.pages, 0, 1, false). | |||
| AddItem(app.menu, 1, 1, false) | |||
| app.initUI() | |||
| app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { | |||
| if event.Key() == utils.AppExitKey.Key { | |||
| app.logger.Info("stopping app") | |||
| app.Stop() | |||
| os.Exit(0) | |||
| } | |||
| if !app.frontScreenHasActiveDialog() { | |||
| event = utils.ParseKeyEventKey(app.logger, event) | |||
| // Next & Previous Screen | |||
| switch event.Rune() { | |||
| case utils.NextScreenKey.Rune(): | |||
| app.switchToNextScreen() | |||
| return nil | |||
| case utils.PreviousScreenKey.Rune(): | |||
| app.switchToPreviousScreen() | |||
| } | |||
| // Normal page key switch | |||
| switch event.Key() { | |||
| case utils.HelpScreenKey.EventKey(): | |||
| app.switchToScreen(app.help.GetTitle()) | |||
| return nil | |||
| case utils.AssetsScreenKey.EventKey(): | |||
| app.switchToScreen(app.assets.GetTitle()) | |||
| return nil | |||
| } | |||
| } | |||
| return event | |||
| }) | |||
| app.currentPage = app.assets.GetTitle() | |||
| app.pages.SwitchToPage(app.currentPage) | |||
| // start the refresh loop | |||
| go app.refresh() | |||
| if err := app.SetRoot(flex, true).SetFocus(app.assets).EnableMouse(false).Run(); err != nil { | |||
| return err | |||
| } | |||
| return nil | |||
| } | |||
| @ -0,0 +1,8 @@ | |||
| package app | |||
| func (app *App) initUI() { | |||
| // Assets page | |||
| app.assets.UpdateShelfData() | |||
| app.assets.UpdateAssetData() | |||
| } | |||
| @ -0,0 +1,44 @@ | |||
| package app | |||
| import ( | |||
| "fmt" | |||
| "strings" | |||
| "git.brettb.xyz/goinv/client/internal/ui/style" | |||
| "github.com/rivo/tview" | |||
| ) | |||
| func makeMenu(menuItems [][]string) *tview.TextView { | |||
| menu := tview.NewTextView(). | |||
| SetDynamicColors(true). | |||
| SetWrap(true). | |||
| SetTextAlign(tview.AlignCenter) | |||
| menu.SetBackgroundColor(style.BgColor) | |||
| var menuList []string | |||
| for i, v := range menuItems { | |||
| key, item := genMenuItem(v) | |||
| if i == len(menuItems)-1 { | |||
| item += " " | |||
| } | |||
| menuList = append(menuList, key+item) | |||
| } | |||
| fmt.Fprintf(menu, "%s", strings.Join(menuList, " ")) | |||
| return menu | |||
| } | |||
| func genMenuItem(items []string) (string, string) { | |||
| key := fmt.Sprintf("[%s::b] <%s>[-:-:-]", style.GetColorHex(style.MenuBgColor), items[0]) | |||
| desc := fmt.Sprintf("[%s:%s:b] %s [-:-:-]", | |||
| style.GetColorHex(style.MenuFgColor), | |||
| style.GetColorHex(style.MenuBgColor), | |||
| strings.ToUpper(items[1])) | |||
| return key, desc | |||
| } | |||
| @ -0,0 +1,19 @@ | |||
| package app | |||
| import ( | |||
| "time" | |||
| "git.brettb.xyz/goinv/client/internal/ui/utils" | |||
| ) | |||
| func (app *App) refresh() { | |||
| app.logger.Sugar().Debugf("starting app refresh loop (interval=%v)", utils.RefreshInterval) | |||
| tick := time.NewTicker(utils.RefreshInterval) | |||
| for { | |||
| <-tick.C | |||
| app.Application.Draw() | |||
| } | |||
| } | |||
| @ -0,0 +1,56 @@ | |||
| package app | |||
| func (app *App) switchToScreen(name string) { | |||
| app.logger.Sugar().Debugf("switching to %s screen", name) | |||
| app.pages.SwitchToPage(name) | |||
| app.setPageFocus(name) | |||
| app.updatePageData(name) | |||
| app.currentPage = name | |||
| } | |||
| func (app *App) frontScreenHasActiveDialog() bool { | |||
| switch app.currentPage { | |||
| case app.assets.GetTitle(): | |||
| return app.assets.SubDialogHasFocus() | |||
| } | |||
| return false | |||
| } | |||
| func (app *App) switchToPreviousScreen() { | |||
| var previousScreen string | |||
| switch app.currentPage { | |||
| case app.help.GetTitle(): | |||
| previousScreen = app.assets.GetTitle() | |||
| case app.assets.GetTitle(): | |||
| previousScreen = app.help.GetTitle() | |||
| } | |||
| app.switchToScreen(previousScreen) | |||
| } | |||
| func (app *App) switchToNextScreen() { | |||
| var nextScreen string | |||
| switch app.currentPage { | |||
| case app.help.GetTitle(): | |||
| nextScreen = app.assets.GetTitle() | |||
| case app.assets.GetTitle(): | |||
| nextScreen = app.help.GetTitle() | |||
| } | |||
| app.switchToScreen(nextScreen) | |||
| } | |||
| func (app *App) setPageFocus(page string) { | |||
| switch page { | |||
| case app.help.GetTitle(): | |||
| app.Application.SetFocus(app.help) | |||
| case app.assets.GetTitle(): | |||
| app.Application.SetFocus(app.assets) | |||
| } | |||
| } | |||
| func (app *App) updatePageData(page string) { | |||
| switch page { | |||
| case app.assets.GetTitle(): | |||
| app.assets.UpdateData() | |||
| } | |||
| } | |||
| @ -0,0 +1,42 @@ | |||
| package types | |||
| import ( | |||
| "time" | |||
| ) | |||
| type AssetResponse struct { | |||
| Asset *Asset `json:"asset"` | |||
| } | |||
| type MultipleAssetsResponse struct { | |||
| Assets []*Asset `json:"assets"` | |||
| Total int `json:"total"` | |||
| } | |||
| type CreateAssetRequest struct { | |||
| Name string `json:"name"` | |||
| Quantity int `json:"quantity"` | |||
| Length string `json:"length,omitempty"` | |||
| Manufacturer string `json:"manufacturer,omitempty"` | |||
| ModelName string `json:"model_name,omitempty"` | |||
| Price float64 `json:"price,omitempty"` | |||
| Comments string `json:"comments,omitempty"` | |||
| ShelfLocationID int `json:"shelf_location_id,omitempty"` | |||
| CategoryID int `json:"category_id,omitempty"` | |||
| } | |||
| type Asset struct { | |||
| ID uint64 `json:"id"` | |||
| Name string `json:"name"` | |||
| Quantity int `json:"quantity"` | |||
| Length string `json:"length,omitempty"` | |||
| Manufacturer string `json:"manufacturer,omitempty"` | |||
| ModelName string `json:"model_name,omitempty"` | |||
| Price float64 `json:"price,omitempty"` | |||
| Comments string `json:"comments,omitempty"` | |||
| ShelfLocation *ShelfLocation `json:"shelf_location,omitempty"` | |||
| Category *Category `json:"category,omitempty"` | |||
| CreatedAt time.Time `json:"created_at"` | |||
| UpdatedAt time.Time `json:"updated_at"` | |||
| DeletedAt *time.Time `json:"deleted_at,omitempty"` | |||
| } | |||
| @ -0,0 +1,11 @@ | |||
| package types | |||
| import "time" | |||
| type Building struct { | |||
| ID uint64 `json:"id"` | |||
| Name string `json:"name"` | |||
| CreatedAt time.Time `json:"created_at"` | |||
| UpdatedAt time.Time `json:"updated_at"` | |||
| DeletedAt *time.Time `json:"deleted_at,omitempty"` | |||
| } | |||
| @ -0,0 +1,11 @@ | |||
| package types | |||
| import "time" | |||
| type Category struct { | |||
| ID uint64 `json:"id"` | |||
| Name string `json:"name"` | |||
| CreatedAt time.Time `json:"created_at"` | |||
| UpdatedAt time.Time `json:"updated_at"` | |||
| DeletedAt *time.Time `json:"deleted_at,omitempty"` | |||
| } | |||
| @ -0,0 +1,23 @@ | |||
| package types | |||
| import "time" | |||
| type ShelfResponse struct { | |||
| ShelfLocation *ShelfLocation `json:"shelf"` | |||
| } | |||
| type MultipleShelfResponse struct { | |||
| ShelfLocations []*ShelfLocation `json:"shelves"` | |||
| Total int `json:"total"` | |||
| } | |||
| type ShelfLocation struct { | |||
| ID uint64 `json:"id"` | |||
| Name string `json:"name"` | |||
| RoomNumber string `json:"room_number,omitempty"` | |||
| Description string `json:"description,omitempty"` | |||
| BuildingID *uint64 `json:"building_id"` | |||
| CreatedAt time.Time `json:"created_at"` | |||
| UpdatedAt time.Time `json:"updated_at"` | |||
| DeletedAt *time.Time `json:"deleted_at,omitempty"` | |||
| } | |||
| @ -0,0 +1,216 @@ | |||
| package assets | |||
| import ( | |||
| "sync" | |||
| "git.brettb.xyz/goinv/client/internal/api" | |||
| "git.brettb.xyz/goinv/client/internal/types" | |||
| "git.brettb.xyz/goinv/client/internal/ui/dialogs" | |||
| "git.brettb.xyz/goinv/client/internal/ui/style" | |||
| "git.brettb.xyz/goinv/client/internal/ui/utils" | |||
| "github.com/gdamore/tcell/v2" | |||
| "github.com/rivo/tview" | |||
| "go.uber.org/zap" | |||
| ) | |||
| const ( | |||
| status_CONFIRM_DELETE_ASSET = "delete_asset" | |||
| ) | |||
| type Assets struct { | |||
| *tview.Box | |||
| client *api.APIClient | |||
| title string | |||
| logger *zap.Logger | |||
| assetTable *tview.Table | |||
| assetList assetListReport | |||
| shelfLocationCache shelfListReport | |||
| assetTableHeaders []string | |||
| assetTableExpansions []int | |||
| cmdDialog *dialogs.CommandDialog | |||
| confirmDialog *dialogs.ConfirmDialog | |||
| errorDialog *dialogs.ErrorDialog | |||
| progressDialog *dialogs.ProgressDialog | |||
| messageDialog *dialogs.MessageDialog | |||
| allDialogs []dialogs.Dialog | |||
| confirmData string | |||
| assetListFunc func() ([]types.Asset, error) | |||
| shelfListFunc func() (map[uint64]types.ShelfLocation, error) | |||
| } | |||
| type assetSelectedItem struct { | |||
| id string | |||
| item string | |||
| quantity string | |||
| shelfLocation string | |||
| manufacturer string | |||
| model string | |||
| category string | |||
| } | |||
| type assetListReport struct { | |||
| mu sync.Mutex | |||
| report []types.Asset | |||
| dirty bool | |||
| } | |||
| type shelfListReport struct { | |||
| mu sync.Mutex | |||
| report map[uint64]types.ShelfLocation | |||
| } | |||
| func NewAssets(logger *zap.Logger, client *api.APIClient) *Assets { | |||
| assets := &Assets{ | |||
| Box: tview.NewBox(), | |||
| client: client, | |||
| title: "assets", | |||
| logger: logger, | |||
| assetTable: tview.NewTable(), | |||
| assetTableHeaders: []string{"id", "item", "quantity", "shelf location", "manufacturer", "model", "category"}, | |||
| assetTableExpansions: []int{1, 4, 1, 2, 2, 2, 2}, | |||
| confirmDialog: dialogs.NewConfirmDialog(logger), | |||
| errorDialog: dialogs.NewErrorDialog(logger), | |||
| progressDialog: dialogs.NewProgressDialog(logger), | |||
| messageDialog: dialogs.NewMessageDialog(logger, ""), | |||
| } | |||
| assets.assetTable.SetBackgroundColor(style.BgColor) | |||
| assets.assetTable.SetBorder(true) | |||
| assets.updateAssetTableTitle(0) | |||
| assets.assetTable.SetTitleColor(style.FgColor) | |||
| assets.assetTable.SetBorderColor(style.BorderColor) | |||
| assets.assetTable.SetFixed(1, 1) | |||
| assets.assetTable.SetSelectable(true, false) | |||
| assets.writeTableHeaders() | |||
| assets.assetTable.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { | |||
| if assets.assetTable.GetRowCount() <= 1 { | |||
| return nil | |||
| } | |||
| return event | |||
| }) | |||
| assets.cmdDialog = dialogs.NewCommandDialog(logger, [][]string{ | |||
| {"create asset", "create a new asset"}, | |||
| {"view asset", "view the selected asset"}, | |||
| {"delete asset", "delete the selected asset"}, | |||
| {"refresh", "refresh the page"}, | |||
| }) | |||
| assets.cmdDialog.SetSelectedFunc(func() { | |||
| assets.cmdDialog.Hide() | |||
| assets.runCommand(assets.cmdDialog.GetSelectedItem()) | |||
| }).SetCancelFunc(func() { | |||
| assets.cmdDialog.Hide() | |||
| }) | |||
| assets.confirmDialog.SetSelectedFunc(func() { | |||
| assets.confirmDialog.Hide() | |||
| switch assets.confirmData { | |||
| case status_CONFIRM_DELETE_ASSET: | |||
| assets.delete() | |||
| } | |||
| }).SetCancelFunc(func() { | |||
| assets.confirmDialog.Hide() | |||
| }) | |||
| assets.messageDialog.SetCancelFunc(func() { | |||
| assets.messageDialog.Hide() | |||
| }) | |||
| assets.SetAssetListFunc(func() ([]types.Asset, error) { | |||
| if asp, err := assets.client.RetrieveAllAssets(); err != nil { | |||
| return nil, err | |||
| } else { | |||
| var aso []types.Asset | |||
| for _, a := range asp { | |||
| aso = append(aso, *a) | |||
| } | |||
| return aso, nil | |||
| } | |||
| }) | |||
| assets.SetShelfListFunc(func() (map[uint64]types.ShelfLocation, error) { | |||
| if resp, err := assets.client.RetrieveAllShelves(); err != nil { | |||
| return nil, err | |||
| } else { | |||
| shelves := map[uint64]types.ShelfLocation{} | |||
| for _, a := range resp { | |||
| shelves[a.ID] = *a | |||
| } | |||
| return shelves, nil | |||
| } | |||
| }) | |||
| assets.allDialogs = []dialogs.Dialog{ | |||
| assets.errorDialog, | |||
| assets.messageDialog, | |||
| assets.progressDialog, | |||
| assets.confirmDialog, | |||
| assets.cmdDialog, | |||
| } | |||
| return assets | |||
| } | |||
| func (a *Assets) GetTitle() string { | |||
| return a.title | |||
| } | |||
| func (a *Assets) HasFocus() bool { | |||
| return dialogs.CheckDialogFocus(a.allDialogs...) || utils.CheckFocus(a.assetTable, a.Box) | |||
| } | |||
| func (a *Assets) SubDialogHasFocus() bool { | |||
| return dialogs.CheckDialogFocus(a.allDialogs...) | |||
| } | |||
| func (a *Assets) Focus(delegate func(tview.Primitive)) { | |||
| for _, dialog := range a.allDialogs { | |||
| if dialog.IsDisplay() { | |||
| delegate(dialog) | |||
| return | |||
| } | |||
| } | |||
| delegate(a.assetTable) | |||
| } | |||
| func (a *Assets) SetAssetListFunc(list func() ([]types.Asset, error)) { | |||
| a.assetListFunc = list | |||
| } | |||
| func (a *Assets) SetShelfListFunc(list func() (map[uint64]types.ShelfLocation, error)) { | |||
| a.shelfListFunc = list | |||
| } | |||
| func (a *Assets) hideAllDialogs() { | |||
| for _, dialog := range a.allDialogs { | |||
| dialog.Hide() | |||
| } | |||
| } | |||
| func (a *Assets) getSelectedItem() *assetSelectedItem { | |||
| selectedItem := assetSelectedItem{} | |||
| if a.assetTable.GetRowCount() <= 1 { | |||
| return nil | |||
| } | |||
| row, _ := a.assetTable.GetSelection() | |||
| selectedItem.id = a.assetTable.GetCell(row, 0).Text | |||
| selectedItem.item = a.assetTable.GetCell(row, 1).Text | |||
| selectedItem.quantity = a.assetTable.GetCell(row, 2).Text | |||
| selectedItem.shelfLocation = a.assetTable.GetCell(row, 3).Text | |||
| selectedItem.manufacturer = a.assetTable.GetCell(row, 4).Text | |||
| selectedItem.model = a.assetTable.GetCell(row, 5).Text | |||
| selectedItem.category = a.assetTable.GetCell(row, 6).Text | |||
| return &selectedItem | |||
| } | |||
| @ -0,0 +1,103 @@ | |||
| package assets | |||
| import ( | |||
| "fmt" | |||
| "git.brettb.xyz/goinv/client/internal/ui/dialogs" | |||
| "git.brettb.xyz/goinv/client/internal/ui/style" | |||
| ) | |||
| func (a *Assets) runCommand(cmd string) { | |||
| switch cmd { | |||
| case "create asset", "view asset": | |||
| a.cNotImplemented() | |||
| return | |||
| case "delete asset": | |||
| a.cdelete() | |||
| return | |||
| case "refresh": | |||
| a.crefresh() | |||
| } | |||
| } | |||
| func (a *Assets) cNotImplemented() { | |||
| a.displayError("not implemented", fmt.Errorf("this command has not been implemented")) | |||
| } | |||
| // Confirm deletion | |||
| func (a *Assets) cdelete() { | |||
| selectedItem := a.getSelectedItem() | |||
| // Empty table | |||
| if selectedItem == nil { | |||
| a.displayError("DELETE ASSET ERROR", fmt.Errorf("no assets to delete")) | |||
| return | |||
| } | |||
| title := "delete asset" | |||
| a.confirmDialog.SetTitle(title) | |||
| a.confirmData = status_CONFIRM_DELETE_ASSET | |||
| bgColor := style.GetColorHex(style.DialogBorderColor) | |||
| fgColor := style.GetColorHex(style.DialogFgColor) | |||
| assetName := fmt.Sprintf("[%s:%s:b]ASSET NAME:[:-:-] %s", fgColor, bgColor, selectedItem.item) | |||
| assetQuantity := fmt.Sprintf(" [%s:%s:b]QUANTITY:[:-:-] %s", fgColor, bgColor, selectedItem.quantity) | |||
| confirmMsg := fmt.Sprintf("%s\n%s\nAre you sure you want to delete the selected asset ?", assetName, assetQuantity) | |||
| a.confirmDialog.SetText(confirmMsg) | |||
| a.confirmDialog.Display() | |||
| } | |||
| func (a *Assets) delete() { | |||
| selectedItem := a.getSelectedItem() | |||
| a.progressDialog.SetTitle(fmt.Sprintf("deleting asset %s", selectedItem.id)) | |||
| a.progressDialog.Display() | |||
| del := func() { | |||
| _, err := a.client.DeleteAssetByID(selectedItem.id) | |||
| a.progressDialog.Hide() | |||
| if err != nil { | |||
| a.displayError("DELETE ASSET ERROR", err) | |||
| return | |||
| } | |||
| // display success message | |||
| a.messageDialog.SetTitle(fmt.Sprintf("deleting asset %s", selectedItem.id)) | |||
| a.messageDialog.SetText(dialogs.MessageGeneric, "Success!", fmt.Sprintf("Asset %s successfully deleted.", selectedItem.id)) | |||
| a.messageDialog.Display() | |||
| a.UpdateAssetData() | |||
| } | |||
| del() | |||
| } | |||
| func (a *Assets) crefresh() { | |||
| a.progressDialog.SetTitle("refreshing assets") | |||
| a.progressDialog.Display() | |||
| ref := func() { | |||
| a.UpdateShelfData() | |||
| a.UpdateAssetData() | |||
| a.progressDialog.Hide() | |||
| if !a.errorDialog.IsDisplay() { | |||
| a.messageDialog.SetTitle(fmt.Sprintf("asset refresh")) | |||
| a.messageDialog.SetText(dialogs.MessageGeneric, "Refreshed!", "Successfully refreshed page.") | |||
| a.messageDialog.Display() | |||
| } | |||
| } | |||
| ref() | |||
| } | |||
| func (a *Assets) displayError(title string, err error) { | |||
| a.errorDialog.SetTitle(title) | |||
| a.errorDialog.SetText(fmt.Sprintf("%v", err)) | |||
| a.errorDialog.Display() | |||
| } | |||
| @ -0,0 +1,48 @@ | |||
| package assets | |||
| import "git.brettb.xyz/goinv/client/internal/types" | |||
| func (a *Assets) UpdateData() { | |||
| a.UpdateShelfData() | |||
| a.UpdateAssetData() | |||
| } | |||
| func (a *Assets) UpdateAssetData() { | |||
| assets, err := a.assetListFunc() | |||
| a.assetList.mu.Lock() | |||
| defer a.assetList.mu.Unlock() | |||
| if err != nil { | |||
| a.displayError("could not retrieve assets", err) | |||
| a.assetList.dirty = true | |||
| return | |||
| } | |||
| a.assetList.dirty = false | |||
| a.assetList.report = assets | |||
| } | |||
| func (a *Assets) UpdateShelfData() { | |||
| shelves, err := a.shelfListFunc() | |||
| if err != nil { | |||
| a.displayError("could not retrieve shelves", err) | |||
| return | |||
| } | |||
| a.shelfLocationCache.mu.Lock() | |||
| a.shelfLocationCache.report = shelves | |||
| a.shelfLocationCache.mu.Unlock() | |||
| } | |||
| func (a *Assets) getAssetData() []types.Asset { | |||
| a.assetList.mu.Lock() | |||
| assetReport := a.assetList.report | |||
| defer a.assetList.mu.Unlock() | |||
| return assetReport | |||
| } | |||
| func (a *Assets) getShelfData() map[uint64]types.ShelfLocation { | |||
| a.shelfLocationCache.mu.Lock() | |||
| shelfReport := a.shelfLocationCache.report | |||
| defer a.shelfLocationCache.mu.Unlock() | |||
| return shelfReport | |||
| } | |||
| @ -0,0 +1,24 @@ | |||
| package assets | |||
| import "github.com/gdamore/tcell/v2" | |||
| func (a *Assets) Draw(screen tcell.Screen) { | |||
| a.refresh() | |||
| a.Box.DrawForSubclass(screen, a) | |||
| x, y, width, height := a.GetInnerRect() | |||
| a.assetTable.SetRect(x, y, width, height) | |||
| a.assetTable.Draw(screen) | |||
| x, y, width, height = a.assetTable.GetInnerRect() | |||
| for _, diag := range a.allDialogs { | |||
| if diag.IsDisplay() { | |||
| diag.SetRect(x, y, width, height) | |||
| diag.Draw(screen) | |||
| return | |||
| } | |||
| } | |||
| } | |||
| @ -0,0 +1,54 @@ | |||
| package assets | |||
| import ( | |||
| "git.brettb.xyz/goinv/client/internal/ui/utils" | |||
| "github.com/gdamore/tcell/v2" | |||
| "github.com/rivo/tview" | |||
| ) | |||
| func (a *Assets) InputHandler() func(*tcell.EventKey, func(tview.Primitive)) { | |||
| return a.WrapInputHandler(func(event *tcell.EventKey, setFocus func(tview.Primitive)) { | |||
| a.logger.Sugar().Debugf("assets event %v received", event) | |||
| if a.progressDialog.IsDisplay() { | |||
| setFocus(a.progressDialog) | |||
| return | |||
| } | |||
| if a.errorDialog.HasFocus() { | |||
| if errorHandler := a.errorDialog.InputHandler(); errorHandler != nil { | |||
| errorHandler(event, setFocus) | |||
| } | |||
| } | |||
| if a.messageDialog.HasFocus() { | |||
| if messageHandler := a.messageDialog.InputHandler(); messageHandler != nil { | |||
| messageHandler(event, setFocus) | |||
| } | |||
| } | |||
| if a.confirmDialog.HasFocus() { | |||
| if confirmHandler := a.confirmDialog.InputHandler(); confirmHandler != nil { | |||
| confirmHandler(event, setFocus) | |||
| } | |||
| } | |||
| if a.cmdDialog.HasFocus() { | |||
| if cmdHandler := a.cmdDialog.InputHandler(); cmdHandler != nil { | |||
| cmdHandler(event, setFocus) | |||
| } | |||
| } | |||
| if a.assetTable.HasFocus() { | |||
| if event.Rune() == utils.CommandMenuKey.Rune() { | |||
| a.cmdDialog.Display() | |||
| } else { | |||
| if tableHandler := a.assetTable.InputHandler(); tableHandler != nil { | |||
| tableHandler(event, setFocus) | |||
| } | |||
| } | |||
| } | |||
| setFocus(a) | |||
| }) | |||
| } | |||
| @ -0,0 +1,97 @@ | |||
| package assets | |||
| import ( | |||
| "fmt" | |||
| "strings" | |||
| "git.brettb.xyz/goinv/client/internal/ui/style" | |||
| "github.com/rivo/tview" | |||
| ) | |||
| const tableHeaderOffset = 1 | |||
| func (a *Assets) refresh() { | |||
| assets := a.getAssetData() | |||
| a.assetTable.Clear() | |||
| a.updateAssetTableTitle(len(assets)) | |||
| a.writeTableHeaders() | |||
| for i, asset := range assets { | |||
| row := i + tableHeaderOffset | |||
| a.assetTable.SetCell(row, 0, | |||
| tview.NewTableCell(fmt.Sprintf("%d", asset.ID)). | |||
| SetExpansion(a.assetTableExpansions[0]). | |||
| SetAlign(tview.AlignLeft)) | |||
| a.assetTable.SetCell(row, 1, | |||
| tview.NewTableCell(asset.Name). | |||
| SetExpansion(a.assetTableExpansions[1]). | |||
| SetAlign(tview.AlignLeft)) | |||
| quantity := "" | |||
| if asset.Quantity < 0 { | |||
| quantity = "DNI" | |||
| } else { | |||
| quantity = fmt.Sprintf("%d", asset.Quantity) | |||
| } | |||
| a.assetTable.SetCell(row, 2, | |||
| tview.NewTableCell(quantity). | |||
| SetExpansion(a.assetTableExpansions[2]). | |||
| SetAlign(tview.AlignLeft)) | |||
| shelfLocation := "" | |||
| if asset.ShelfLocation != nil { | |||
| shelfLocation = asset.ShelfLocation.Name | |||
| } | |||
| a.assetTable.SetCell(row, 3, | |||
| tview.NewTableCell(shelfLocation). | |||
| SetExpansion(a.assetTableExpansions[3]). | |||
| SetAlign(tview.AlignLeft)) | |||
| a.assetTable.SetCell(row, 4, | |||
| tview.NewTableCell(asset.Manufacturer). | |||
| SetExpansion(a.assetTableExpansions[4]). | |||
| SetAlign(tview.AlignLeft)) | |||
| a.assetTable.SetCell(row, 5, | |||
| tview.NewTableCell(asset.ModelName). | |||
| SetExpansion(a.assetTableExpansions[5]). | |||
| SetAlign(tview.AlignLeft)) | |||
| category := "" | |||
| if asset.Category != nil { | |||
| category = asset.Category.Name | |||
| } | |||
| a.assetTable.SetCell(row, 6, | |||
| tview.NewTableCell(category). | |||
| SetExpansion(a.assetTableExpansions[6]). | |||
| SetAlign(tview.AlignLeft)) | |||
| } | |||
| } | |||
| func (a *Assets) updateAssetTableTitle(count int) { | |||
| dirtyFlag := "" | |||
| if a.assetList.dirty { | |||
| dirtyFlag = "*" | |||
| } | |||
| title := fmt.Sprintf("[::b]ASSETS [%s%d]", dirtyFlag, count) | |||
| a.assetTable.SetTitle(title) | |||
| } | |||
| func (a *Assets) writeTableHeaders() { | |||
| for i, headerText := range a.assetTableHeaders { | |||
| header := fmt.Sprintf("[::b]%s", strings.ToUpper(headerText)) | |||
| a.assetTable.SetCell(0, i, | |||
| tview.NewTableCell(header). | |||
| SetExpansion(a.assetTableExpansions[i]). | |||
| SetBackgroundColor(style.TableHeaderBgColor). | |||
| SetTextColor(style.TableHeaderFgColor). | |||
| SetAlign(tview.AlignLeft). | |||
| SetSelectable(false)) | |||
| } | |||
| } | |||
| @ -0,0 +1,300 @@ | |||
| package dialogs | |||
| import ( | |||
| "fmt" | |||
| "git.brettb.xyz/goinv/client/internal/ui/style" | |||
| "git.brettb.xyz/goinv/client/internal/ui/utils" | |||
| "github.com/gdamore/tcell/v2" | |||
| "github.com/rivo/tview" | |||
| "go.uber.org/zap" | |||
| ) | |||
| const ( | |||
| cmdWidthOffset = 6 | |||
| ) | |||
| const ( | |||
| cmdTableFocus = 0 + iota | |||
| cmdFormFocus | |||
| ) | |||
| type CommandDialog struct { | |||
| *tview.Box | |||
| layout *tview.Flex | |||
| table *tview.Table | |||
| form *tview.Form | |||
| logger *zap.Logger | |||
| display bool | |||
| options [][]string | |||
| width int | |||
| height int | |||
| focusElement int | |||
| selectedStyle tcell.Style | |||
| cancelHandler func() | |||
| selectHandler func() | |||
| } | |||
| func NewCommandDialog(logger *zap.Logger, options [][]string) *CommandDialog { | |||
| form := tview.NewForm(). | |||
| AddButton("Cancel", nil). | |||
| SetButtonsAlign(tview.AlignRight) | |||
| form.SetBackgroundColor(style.DialogBgColor) | |||
| form.SetButtonBackgroundColor(style.ButtonBgColor) | |||
| form.SetButtonTextColor(style.ButtonFgColor) | |||
| activatedStyle := tcell.StyleDefault. | |||
| Background(style.ButtonSelectedBgColor). | |||
| Foreground(style.ButtonSelectedFgColor) | |||
| form.SetButtonActivatedStyle(activatedStyle) | |||
| cmdsTable := tview.NewTable() | |||
| cmdsTable.SetBackgroundColor(style.DialogBgColor) | |||
| cmdWidth := 0 | |||
| cmdsTable.SetCell(0, 0, | |||
| tview.NewTableCell(fmt.Sprintf("[%s::b]COMMAND", style.GetColorHex(style.TableHeaderFgColor))). | |||
| SetExpansion(1). | |||
| SetBackgroundColor(style.TableHeaderBgColor). | |||
| SetTextColor(style.TableHeaderFgColor). | |||
| SetAlign(tview.AlignLeft). | |||
| SetSelectable(false)) | |||
| cmdsTable.SetCell(0, 1, | |||
| tview.NewTableCell(fmt.Sprintf("[%s::b]DESCRIPTION", style.GetColorHex(style.TableHeaderFgColor))). | |||
| SetExpansion(1). | |||
| SetBackgroundColor(style.TableHeaderBgColor). | |||
| SetTextColor(style.TableHeaderFgColor). | |||
| SetAlign(tview.AlignCenter). | |||
| SetSelectable(false)) | |||
| col1Width := 0 | |||
| col2Width := 0 | |||
| for i, option := range options { | |||
| cmdsTable.SetCell(i+1, 0, | |||
| tview.NewTableCell(option[0]). | |||
| SetAlign(tview.AlignLeft). | |||
| SetSelectable(true).SetTextColor(style.DialogFgColor)) | |||
| cmdsTable.SetCell(i+1, 1, | |||
| tview.NewTableCell(option[1]). | |||
| SetAlign(tview.AlignLeft). | |||
| SetSelectable(true).SetTextColor(style.DialogFgColor)) | |||
| if len(option[0]) > col1Width { | |||
| col1Width = len(option[0]) | |||
| } | |||
| if len(option[1]) > col2Width { | |||
| col2Width = len(option[1]) | |||
| } | |||
| } | |||
| cmdWidth = col1Width + col2Width + 2 | |||
| cmdsTable.SetFixed(1, 1) | |||
| cmdsTable.SetSelectable(true, false) | |||
| cmdsTable.SetBackgroundColor(style.DialogBgColor) | |||
| cmdLayout := tview.NewFlex().SetDirection(tview.FlexColumn) | |||
| cmdLayout.AddItem(utils.EmptyBoxSpace(style.DialogBgColor), 1, 0, false) | |||
| cmdLayout.AddItem(cmdsTable, 0, 1, true) | |||
| cmdLayout.AddItem(utils.EmptyBoxSpace(style.DialogBgColor), 1, 0, false) | |||
| layout := tview.NewFlex().SetDirection(tview.FlexRow) | |||
| layout.AddItem(cmdLayout, 0, 1, true) | |||
| layout.AddItem(form, DialogFormHeight, 0, true) | |||
| layout.SetBorder(true) | |||
| layout.SetBorderColor(style.DialogBorderColor) | |||
| layout.SetBackgroundColor(style.DialogBgColor) | |||
| selectedStyle := tcell.StyleDefault. | |||
| Background(style.TableSelectedBgColor). | |||
| Foreground(style.TableSelectedFgColor) | |||
| cmdsTable.SetSelectedStyle(selectedStyle) | |||
| return &CommandDialog{ | |||
| Box: tview.NewBox().SetBorder(false), | |||
| layout: layout, | |||
| table: cmdsTable, | |||
| form: form, | |||
| display: false, | |||
| options: options, | |||
| width: cmdWidth + cmdWidthOffset, | |||
| height: len(options) + TableHeightOffset + DialogFormHeight, | |||
| focusElement: cmdTableFocus, | |||
| selectedStyle: selectedStyle, | |||
| logger: logger, | |||
| } | |||
| } | |||
| // GetSelectedItem returns selected row item. | |||
| func (cmd *CommandDialog) GetSelectedItem() string { | |||
| row, _ := cmd.table.GetSelection() | |||
| if row >= 0 { | |||
| return cmd.options[row-1][0] | |||
| } | |||
| return "" | |||
| } | |||
| // GetCommandCount returns number of commands | |||
| func (cmd *CommandDialog) GetCommandCount() int { | |||
| return cmd.table.GetRowCount() | |||
| } | |||
| // Display this primitive. | |||
| func (cmd *CommandDialog) Display() { | |||
| cmd.table.Select(1, 0) | |||
| cmd.form.SetFocus(1) | |||
| cmd.display = true | |||
| } | |||
| // Hide this primitive | |||
| func (cmd *CommandDialog) Hide() { | |||
| cmd.display = false | |||
| cmd.focusElement = cmdTableFocus | |||
| cmd.table.SetSelectedStyle(cmd.selectedStyle) | |||
| } | |||
| // HasFocus returns whether this primitive has focus | |||
| func (cmd *CommandDialog) HasFocus() bool { | |||
| return utils.CheckFocus(cmd.table, cmd.form) | |||
| } | |||
| func (cmd *CommandDialog) IsDisplay() bool { | |||
| return cmd.display | |||
| } | |||
| func (cmd *CommandDialog) Focus(delegate func(tview.Primitive)) { | |||
| if cmd.focusElement == cmdTableFocus { | |||
| delegate(cmd.table) | |||
| return | |||
| } | |||
| button := cmd.form.GetButton(cmd.form.GetButtonCount() - 1) | |||
| button.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { | |||
| if event.Key() == utils.SwitchFocusKey.Key { | |||
| cmd.focusElement = cmdTableFocus | |||
| cmd.Focus(delegate) | |||
| cmd.form.SetFocus(0) | |||
| return nil | |||
| } | |||
| return event | |||
| }) | |||
| delegate(cmd.form) | |||
| } | |||
| func (cmd *CommandDialog) InputHandler() func(event *tcell.EventKey, setFocus func(primitive tview.Primitive)) { | |||
| return cmd.WrapInputHandler(func(event *tcell.EventKey, setFocus func(primitive tview.Primitive)) { | |||
| cmd.logger.Sugar().Debugf("command dialog event %v received", event) | |||
| if event.Key() == utils.CloseDialogKey.Key { | |||
| cmd.cancelHandler() | |||
| return | |||
| } | |||
| if event.Key() == utils.SwitchFocusKey.Key { | |||
| cmd.setFocusElement() | |||
| } | |||
| if cmd.form.HasFocus() { | |||
| if formHandler := cmd.form.InputHandler(); formHandler != nil { | |||
| formHandler(event, setFocus) | |||
| return | |||
| } | |||
| } | |||
| if cmd.table.HasFocus() { | |||
| if event.Key() == tcell.KeyEnter { | |||
| cmd.selectHandler() | |||
| return | |||
| } | |||
| if tableHandler := cmd.table.InputHandler(); tableHandler != nil { | |||
| tableHandler(event, setFocus) | |||
| return | |||
| } | |||
| } | |||
| }) | |||
| } | |||
| // SetSelectedFunc sets the form enter button selected function | |||
| func (cmd *CommandDialog) SetSelectedFunc(handler func()) *CommandDialog { | |||
| cmd.selectHandler = handler | |||
| return cmd | |||
| } | |||
| // SetCancelFunc sets form cancel button selected function. | |||
| func (cmd *CommandDialog) SetCancelFunc(handler func()) *CommandDialog { | |||
| cmd.cancelHandler = handler | |||
| cancelButton := cmd.form.GetButton(cmd.form.GetButtonCount() - 1) | |||
| cancelButton.SetSelectedFunc(handler) | |||
| return cmd | |||
| } | |||
| // SetRect set rects for this primitive | |||
| func (cmd *CommandDialog) SetRect(x, y, width, height int) { | |||
| ws := (width - cmd.width) / 2 | |||
| hs := (height - cmd.height) / 2 | |||
| dy := y + hs | |||
| bWidth := cmd.width | |||
| if cmd.width > width { | |||
| ws = 0 | |||
| bWidth = width - 1 | |||
| } | |||
| bHeight := cmd.height | |||
| if cmd.height > height { | |||
| dy = y + 1 | |||
| bHeight = height - 1 | |||
| } | |||
| cmd.Box.SetRect(x+ws, dy, bWidth, bHeight) | |||
| x, y, width, height = cmd.Box.GetInnerRect() | |||
| cmd.layout.SetRect(x, y, width, height) | |||
| } | |||
| func (cmd *CommandDialog) Draw(screen tcell.Screen) { | |||
| if !cmd.display { | |||
| return | |||
| } | |||
| cmd.Box.DrawForSubclass(screen, cmd) | |||
| cmd.layout.Draw(screen) | |||
| } | |||
| func (cmd *CommandDialog) setFocusElement() { | |||
| if cmd.focusElement == cmdTableFocus { | |||
| cmd.focusElement = cmdFormFocus | |||
| cmd.table.SetSelectedStyle(tcell.StyleDefault. | |||
| Background(style.DialogBgColor). | |||
| Foreground(style.DialogFgColor)) | |||
| } else { | |||
| cmd.focusElement = cmdTableFocus | |||
| cmd.table.SetSelectedStyle(cmd.selectedStyle) | |||
| } | |||
| } | |||
| @ -0,0 +1,203 @@ | |||
| package dialogs | |||
| import ( | |||
| "strings" | |||
| "git.brettb.xyz/goinv/client/internal/ui/style" | |||
| "git.brettb.xyz/goinv/client/internal/ui/utils" | |||
| "github.com/gdamore/tcell/v2" | |||
| "github.com/rivo/tview" | |||
| "go.uber.org/zap" | |||
| ) | |||
| type ConfirmDialog struct { | |||
| *tview.Box | |||
| logger *zap.Logger | |||
| layout *tview.Flex | |||
| textview *tview.TextView | |||
| form *tview.Form | |||
| x int | |||
| y int | |||
| width int | |||
| height int | |||
| message string | |||
| display bool | |||
| cancelHandler func() | |||
| selectHandler func() | |||
| } | |||
| func NewConfirmDialog(logger *zap.Logger) *ConfirmDialog { | |||
| dialog := &ConfirmDialog{ | |||
| Box: tview.NewBox(), | |||
| logger: logger, | |||
| display: false, | |||
| } | |||
| dialog.textview = tview.NewTextView(). | |||
| SetDynamicColors(true). | |||
| SetWrap(true). | |||
| SetTextAlign(tview.AlignLeft) | |||
| dialog.textview.SetBackgroundColor(style.DialogBgColor) | |||
| dialog.textview.SetTextColor(style.DialogFgColor) | |||
| dialog.form = tview.NewForm(). | |||
| AddButton("Cancel", nil). | |||
| AddButton(" OK ", nil). | |||
| SetButtonsAlign(tview.AlignRight) | |||
| dialog.form.SetBackgroundColor(style.DialogBgColor) | |||
| dialog.form.SetButtonBackgroundColor(style.ButtonBgColor) | |||
| dialog.form.SetButtonTextColor(style.ButtonFgColor) | |||
| activatedStyle := tcell.StyleDefault. | |||
| Background(style.ButtonSelectedBgColor). | |||
| Foreground(style.ButtonSelectedFgColor) | |||
| dialog.form.SetButtonActivatedStyle(activatedStyle) | |||
| dialog.layout = tview.NewFlex().SetDirection(tview.FlexRow) | |||
| dialog.layout.SetBorder(true) | |||
| dialog.layout.SetBorderColor(style.DialogBorderColor) | |||
| dialog.layout.SetBackgroundColor(style.DialogBgColor) | |||
| return dialog | |||
| } | |||
| func (d *ConfirmDialog) Display() { | |||
| d.display = true | |||
| d.form.SetFocus(1) | |||
| } | |||
| func (d *ConfirmDialog) IsDisplay() bool { | |||
| return d.display | |||
| } | |||
| func (d *ConfirmDialog) Hide() { | |||
| d.textview.SetText("") | |||
| d.message = "" | |||
| d.display = false | |||
| } | |||
| func (d *ConfirmDialog) SetTitle(title string) { | |||
| d.layout.SetTitle(strings.ToUpper(title)) | |||
| d.layout.SetTitleColor(style.DialogFgColor) | |||
| } | |||
| func (d *ConfirmDialog) SetText(message string) { | |||
| d.message = message | |||
| d.textview.Clear() | |||
| msg := "\n" + message | |||
| d.textview.SetText(msg) | |||
| d.textview.ScrollToBeginning() | |||
| d.setRect() | |||
| } | |||
| func (d *ConfirmDialog) Focus(delegate func(tview.Primitive)) { | |||
| delegate(d.form) | |||
| } | |||
| func (d *ConfirmDialog) HasFocus() bool { | |||
| return d.form.HasFocus() | |||
| } | |||
| func (d *ConfirmDialog) SetRect(x, y, width, height int) { | |||
| d.x = x + DialogPadding | |||
| d.y = y + DialogPadding | |||
| d.width = width - (2 * DialogPadding) //nolint:gomnd | |||
| d.height = height - (2 * DialogPadding) //nolint:gomnd | |||
| d.setRect() | |||
| } | |||
| func (d *ConfirmDialog) setRect() { | |||
| maxHeight := d.height | |||
| maxWidth := d.width | |||
| messageHeight := len(strings.Split(d.message, "\n")) | |||
| messageWidth := getMessageWidth(d.message) | |||
| layoutHeight := messageHeight + 2 //nolint:gomnd | |||
| if maxHeight > layoutHeight+DialogFormHeight { | |||
| d.height = layoutHeight + DialogFormHeight + 2 //nolint:gomnd | |||
| } else { | |||
| d.height = maxHeight | |||
| layoutHeight = d.height - DialogFormHeight - 2 //nolint:gomnd | |||
| } | |||
| if maxHeight > d.height { | |||
| emptyHeight := (maxHeight - d.height) / 2 //nolint:gomnd | |||
| d.y += emptyHeight | |||
| } | |||
| if d.width > DialogMinWidth { | |||
| if messageWidth < DialogMinWidth { | |||
| d.width = DialogMinWidth + 2 //nolint:gomnd | |||
| } else if messageWidth < d.width { | |||
| d.width = messageWidth + 2 //nolint:gomnd | |||
| } | |||
| } | |||
| if maxWidth > d.width { | |||
| emptyWidth := (maxWidth - d.width) / 2 //nolint:gomnd | |||
| d.x += emptyWidth | |||
| } | |||
| msgLayout := tview.NewFlex().SetDirection(tview.FlexColumn) | |||
| msgLayout.AddItem(utils.EmptyBoxSpace(style.DialogBgColor), 1, 0, false) | |||
| msgLayout.AddItem(d.textview, 0, 1, true) | |||
| msgLayout.AddItem(utils.EmptyBoxSpace(style.DialogBgColor), 1, 0, false) | |||
| d.layout.Clear() | |||
| d.layout.AddItem(msgLayout, layoutHeight, 0, true) | |||
| d.layout.AddItem(d.form, DialogFormHeight, 0, true) | |||
| d.Box.SetRect(d.x, d.y, d.width, d.height) | |||
| } | |||
| // Draw draws this primitive onto the screen. | |||
| func (d *ConfirmDialog) Draw(screen tcell.Screen) { | |||
| if !d.display { | |||
| return | |||
| } | |||
| d.Box.DrawForSubclass(screen, d) | |||
| x, y, width, height := d.Box.GetInnerRect() | |||
| d.layout.SetRect(x, y, width, height) | |||
| d.layout.Draw(screen) | |||
| } | |||
| func (d *ConfirmDialog) InputHandler() func(*tcell.EventKey, func(tview.Primitive)) { | |||
| return d.WrapInputHandler(func(event *tcell.EventKey, setFocus func(tview.Primitive)) { | |||
| d.logger.Sugar().Debugf("confirm dialog event %v received", event) | |||
| if event.Key() == utils.CloseDialogKey.EventKey() { | |||
| d.cancelHandler() | |||
| return | |||
| } | |||
| if formHandler := d.form.InputHandler(); formHandler != nil { | |||
| formHandler(event, setFocus) | |||
| return | |||
| } | |||
| }) | |||
| } | |||
| func (d *ConfirmDialog) SetCancelFunc(handler func()) *ConfirmDialog { | |||
| d.cancelHandler = handler | |||
| cancelButton := d.form.GetButton(d.form.GetButtonCount() - 2) | |||
| cancelButton.SetSelectedFunc(handler) | |||
| return d | |||
| } | |||
| func (d *ConfirmDialog) SetSelectedFunc(handler func()) *ConfirmDialog { | |||
| d.selectHandler = handler | |||
| enterButton := d.form.GetButton(d.form.GetButtonCount() - 1) | |||
| enterButton.SetSelectedFunc(handler) | |||
| return d | |||
| } | |||
| @ -0,0 +1,106 @@ | |||
| package dialogs | |||
| import ( | |||
| "fmt" | |||
| "git.brettb.xyz/goinv/client/internal/ui/style" | |||
| "github.com/gdamore/tcell/v2" | |||
| "github.com/rivo/tview" | |||
| "go.uber.org/zap" | |||
| ) | |||
| type ErrorDialog struct { | |||
| *tview.Box | |||
| logger *zap.Logger | |||
| modal *tview.Modal | |||
| title string | |||
| message string | |||
| display bool | |||
| } | |||
| func NewErrorDialog(logger *zap.Logger) *ErrorDialog { | |||
| bgColor := style.ErrorDialogBgColor | |||
| dialog := ErrorDialog{ | |||
| Box: tview.NewBox(), | |||
| logger: logger, | |||
| modal: tview.NewModal().SetBackgroundColor(bgColor).AddButtons([]string{"OK"}), | |||
| display: false, | |||
| } | |||
| dialog.modal.SetButtonBackgroundColor(style.ErrorDialogButtonBgColor) | |||
| dialog.modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) { | |||
| dialog.Hide() | |||
| }) | |||
| return &dialog | |||
| } | |||
| func (e *ErrorDialog) Display() { | |||
| e.display = true | |||
| } | |||
| func (e *ErrorDialog) Hide() { | |||
| e.SetText("") | |||
| e.title = "" | |||
| e.message = "" | |||
| e.display = false | |||
| } | |||
| func (e *ErrorDialog) IsDisplay() bool { | |||
| return e.display | |||
| } | |||
| func (e *ErrorDialog) SetText(message string) { | |||
| e.message = message | |||
| } | |||
| func (e *ErrorDialog) SetTitle(title string) { | |||
| e.title = title | |||
| } | |||
| func (e *ErrorDialog) HasFocus() bool { | |||
| return e.modal.HasFocus() | |||
| } | |||
| func (e *ErrorDialog) Focus(delegate func(tview.Primitive)) { | |||
| delegate(e.modal) | |||
| } | |||
| func (e *ErrorDialog) InputHandler() func(*tcell.EventKey, func(tview.Primitive)) { | |||
| return e.WrapInputHandler(func(event *tcell.EventKey, setFocus func(tview.Primitive)) { | |||
| e.logger.Sugar().Debugf("error dialog event %v received", event) | |||
| if modalHandler := e.modal.InputHandler(); modalHandler != nil { | |||
| modalHandler(event, setFocus) | |||
| return | |||
| } | |||
| }) | |||
| } | |||
| func (e *ErrorDialog) SetRect(x, y, width, height int) { | |||
| e.Box.SetRect(x, y, width, height) | |||
| } | |||
| func (e *ErrorDialog) Draw(screen tcell.Screen) { | |||
| hFgColor := style.FgColor | |||
| headerColor := style.GetColorHex(hFgColor) | |||
| var errorMessage string | |||
| if e.title != "" { | |||
| errorMessage = fmt.Sprintf("[%s::b]%s[-::-]\n", headerColor, e.title) | |||
| } | |||
| errorMessage += e.message | |||
| e.modal.SetText(errorMessage) | |||
| e.modal.Draw(screen) | |||
| } | |||
| func (e *ErrorDialog) SetDoneFunc(handler func()) *ErrorDialog { | |||
| e.modal.SetDoneFunc(func(buttonIndex int, buttonLabel string) { | |||
| handler() | |||
| }) | |||
| return e | |||
| } | |||
| @ -0,0 +1,238 @@ | |||
| package dialogs | |||
| import ( | |||
| "strings" | |||
| "git.brettb.xyz/goinv/client/internal/ui/style" | |||
| "git.brettb.xyz/goinv/client/internal/ui/utils" | |||
| "github.com/gdamore/tcell/v2" | |||
| "github.com/rivo/tview" | |||
| "go.uber.org/zap" | |||
| ) | |||
| type MessageDialog struct { | |||
| *tview.Box | |||
| logger *zap.Logger | |||
| layout *tview.Flex | |||
| infoType *tview.InputField | |||
| textView *tview.TextView | |||
| form *tview.Form | |||
| display bool | |||
| message string | |||
| cancelHandler func() | |||
| } | |||
| type messageInfo int | |||
| const ( | |||
| MessageGeneric messageInfo = iota | |||
| ) | |||
| func NewMessageDialog(logger *zap.Logger, text string) *MessageDialog { | |||
| dialog := MessageDialog{ | |||
| Box: tview.NewBox(), | |||
| logger: logger, | |||
| infoType: tview.NewInputField(), | |||
| display: false, | |||
| message: text, | |||
| } | |||
| dialog.infoType.SetBackgroundColor(style.ButtonBgColor) | |||
| dialog.infoType.SetFieldStyle(tcell.StyleDefault. | |||
| Background(style.ButtonBgColor). | |||
| Foreground(style.ButtonFgColor)) | |||
| dialog.infoType.SetLabelStyle(tcell.StyleDefault. | |||
| Background(style.ButtonBgColor). | |||
| Foreground(style.ButtonFgColor)) | |||
| dialog.textView = tview.NewTextView(). | |||
| SetDynamicColors(true). | |||
| SetWrap(true). | |||
| SetTextAlign(tview.AlignLeft) | |||
| dialog.textView.SetBackgroundColor(style.DialogSubBoxBgColor) | |||
| dialog.textView.SetBorderColor(style.DialogSubBoxBorderColor) | |||
| dialog.textView.SetBorder(true) | |||
| dialog.textView.SetTextStyle(tcell.StyleDefault. | |||
| Background(style.DialogSubBoxBgColor). | |||
| Foreground(style.DialogSubBoxFgColor)) | |||
| tlayout := tview.NewFlex().SetDirection(tview.FlexColumn) | |||
| tlayout.AddItem(utils.EmptyBoxSpace(style.DialogBgColor), 1, 0, false) | |||
| tlayout.AddItem(tview.NewFlex().SetDirection(tview.FlexRow). | |||
| AddItem(dialog.infoType, 1, 0, false). | |||
| AddItem(utils.EmptyBoxSpace(style.DialogBgColor), 1, 0, false). | |||
| AddItem(dialog.textView, 0, 1, true), | |||
| 0, 1, true) | |||
| tlayout.AddItem(utils.EmptyBoxSpace(style.DialogBgColor), 1, 0, false) | |||
| dialog.form = tview.NewForm(). | |||
| AddButton("Cancel", nil). | |||
| SetButtonsAlign(tview.AlignRight) | |||
| dialog.form.SetFocus(0) | |||
| dialog.form.SetBackgroundColor(style.DialogBgColor) | |||
| dialog.form.SetButtonBackgroundColor(style.ButtonBgColor) | |||
| dialog.form.SetButtonTextColor(style.ButtonFgColor) | |||
| dialog.form.SetButtonActivatedStyle(tcell.StyleDefault. | |||
| Background(style.ButtonSelectedBgColor). | |||
| Foreground(style.ButtonSelectedFgColor)) | |||
| dialog.layout = tview.NewFlex().SetDirection(tview.FlexRow) | |||
| dialog.layout.AddItem(utils.EmptyBoxSpace(style.DialogBgColor), 1, 0, false) | |||
| dialog.layout.AddItem(tlayout, 0, 1, true) | |||
| dialog.layout.AddItem(dialog.form, DialogFormHeight, 0, true) | |||
| dialog.layout.SetBorder(true) | |||
| dialog.layout.SetBorderColor(style.DialogBorderColor) | |||
| dialog.layout.SetBackgroundColor(style.DialogBgColor) | |||
| dialog.layout.SetTitleColor(style.DialogFgColor) | |||
| return &dialog | |||
| } | |||
| func (d *MessageDialog) Display() { | |||
| d.display = true | |||
| } | |||
| func (d *MessageDialog) IsDisplay() bool { | |||
| return d.display | |||
| } | |||
| func (d *MessageDialog) Hide() { | |||
| d.message = "" | |||
| d.textView.SetText("") | |||
| d.display = false | |||
| } | |||
| func (d *MessageDialog) SetTitle(title string) { | |||
| d.layout.SetTitle(strings.ToUpper(title)) | |||
| } | |||
| func (d *MessageDialog) SetText(headerType messageInfo, headerMessage string, message string) { | |||
| msgTypeLabel := "" | |||
| switch headerType { | |||
| case MessageGeneric: | |||
| msgTypeLabel = "SYSTEM:" | |||
| } | |||
| if msgTypeLabel != "" { | |||
| d.infoType.SetLabel("[::b]" + msgTypeLabel) | |||
| d.infoType.SetLabelWidth(len(msgTypeLabel) + 1) | |||
| d.infoType.SetText(headerMessage) | |||
| } | |||
| d.message = message | |||
| d.textView.Clear() | |||
| if d.message == "" { | |||
| d.textView.SetBorder(false) | |||
| d.textView.SetText("") | |||
| } else { | |||
| //d.textView.SetTextColor(style.DialogFgColor) | |||
| //d.textView.SetBackgroundColor(style.DialogSubBoxBorderColor) | |||
| //d.textView.SetBorder(true) | |||
| //d.textView.SetBorderColor(style.DialogBorderColor) | |||
| //d.textView.SetTextStyle(tcell.StyleDefault. | |||
| // Background(style.DialogSubBoxBorderColor). | |||
| // Foreground(style.ButtonFgColor)) | |||
| d.textView.SetBorder(true) | |||
| d.textView.SetText(message) | |||
| } | |||
| d.textView.ScrollToBeginning() | |||
| } | |||
| func (d *MessageDialog) TextScrollToEnd() { | |||
| d.textView.ScrollToEnd() | |||
| } | |||
| func (d *MessageDialog) Focus(delegate func(tview.Primitive)) { | |||
| delegate(d.form) | |||
| } | |||
| func (d *MessageDialog) HasFocus() bool { | |||
| return utils.CheckFocus(d.form, d.textView, d.Box) | |||
| } | |||
| func (d *MessageDialog) SetRect(x, y, width, height int) { | |||
| messageHeight := 0 | |||
| if d.message != "" { | |||
| messageHeight = len(strings.Split(d.message, "\n")) | |||
| } | |||
| messageWidth := getMessageWidth(d.message) | |||
| headerWidth := len(d.infoType.GetText()) + len(d.infoType.GetLabel()) + 4 | |||
| if messageWidth < headerWidth { | |||
| messageWidth = headerWidth | |||
| } | |||
| dWidth := width - (2 * DialogPadding) | |||
| if messageWidth+4 < dWidth { | |||
| dWidth = messageWidth + 4 | |||
| } | |||
| if DialogMinWidth < width && dWidth < DialogMinWidth { | |||
| dWidth = DialogMinWidth | |||
| } | |||
| emptySpace := (width - dWidth) / 2 | |||
| dX := x + emptySpace | |||
| dHeight := messageHeight + DialogFormHeight + DialogPadding + 4 | |||
| if dHeight > height { | |||
| dHeight = height - DialogPadding - 1 | |||
| } | |||
| textviewHeight := dHeight - DialogFormHeight - 2 | |||
| hs := (height - dHeight) / 2 | |||
| dY := y + hs | |||
| d.Box.SetRect(dX, dY, dWidth, dHeight) | |||
| d.layout.ResizeItem(d.textView, textviewHeight, 0) | |||
| } | |||
| func (d *MessageDialog) Draw(screen tcell.Screen) { | |||
| if !d.display { | |||
| return | |||
| } | |||
| d.Box.DrawForSubclass(screen, d) | |||
| x, y, width, height := d.Box.GetInnerRect() | |||
| d.layout.SetRect(x, y, width, height) | |||
| d.layout.Draw(screen) | |||
| } | |||
| func (d *MessageDialog) InputHandler() func(*tcell.EventKey, func(tview.Primitive)) { | |||
| return d.WrapInputHandler(func(event *tcell.EventKey, setFocus func(tview.Primitive)) { | |||
| d.logger.Sugar().Debugf("message dialog event %v received", event) | |||
| if event.Key() == utils.CloseDialogKey.EventKey() || event.Key() == tcell.KeyEnter { | |||
| d.cancelHandler() | |||
| return | |||
| } | |||
| if event.Key() == utils.SwitchFocusKey.EventKey() { | |||
| if formHandler := d.form.InputHandler(); formHandler != nil { | |||
| formHandler(event, setFocus) | |||
| return | |||
| } | |||
| } | |||
| if textHandler := d.textView.InputHandler(); textHandler != nil { | |||
| textHandler(event, setFocus) | |||
| return | |||
| } | |||
| }) | |||
| } | |||
| func (d *MessageDialog) SetCancelFunc(handler func()) *MessageDialog { | |||
| d.cancelHandler = handler | |||
| return d | |||
| } | |||
| @ -0,0 +1,128 @@ | |||
| package dialogs | |||
| import ( | |||
| "fmt" | |||
| "strings" | |||
| "git.brettb.xyz/goinv/client/internal/ui/style" | |||
| "github.com/gdamore/tcell/v2" | |||
| "github.com/rivo/tview" | |||
| "go.uber.org/zap" | |||
| ) | |||
| const ( | |||
| prgCell = "▉" | |||
| prgMinWidth = 40 | |||
| ) | |||
| type ProgressDialog struct { | |||
| *tview.Box | |||
| logger *zap.Logger | |||
| x int | |||
| y int | |||
| width int | |||
| height int | |||
| counterValue int | |||
| display bool | |||
| } | |||
| func NewProgressDialog(logger *zap.Logger) *ProgressDialog { | |||
| return &ProgressDialog{ | |||
| logger: logger, | |||
| Box: tview.NewBox(). | |||
| SetBorder(true). | |||
| SetBorderColor(style.DialogBorderColor), | |||
| display: false, | |||
| } | |||
| } | |||
| func (d *ProgressDialog) SetTitle(title string) { | |||
| d.Box.SetTitle(title) | |||
| } | |||
| func (d *ProgressDialog) Draw(screen tcell.Screen) { | |||
| if !d.display || d.height < 3 { | |||
| return | |||
| } | |||
| d.Box.DrawForSubclass(screen, d) | |||
| x, y, width, _ := d.Box.GetInnerRect() | |||
| tickStr := d.tickStr(width) | |||
| tview.Print(screen, tickStr, x, y, width, tview.AlignLeft, style.PrgBarBgColor) | |||
| } | |||
| func (d *ProgressDialog) SetRect(x, y, width, height int) { | |||
| d.x = x | |||
| d.y = y | |||
| d.width = width | |||
| if d.width > prgMinWidth { | |||
| d.width = prgMinWidth | |||
| spaceWidth := (width - d.width) / 2 | |||
| d.x = x + spaceWidth | |||
| } | |||
| if height > 3 { | |||
| d.height = 3 | |||
| spaceHeight := (height - d.height) / 2 | |||
| d.y = y + spaceHeight | |||
| } | |||
| d.Box.SetRect(d.x, d.y, d.width, d.height) | |||
| } | |||
| func (d *ProgressDialog) Hide() { | |||
| d.display = false | |||
| } | |||
| func (d *ProgressDialog) Display() { | |||
| d.counterValue = 0 | |||
| d.display = true | |||
| } | |||
| func (d *ProgressDialog) IsDisplay() bool { | |||
| return d.display | |||
| } | |||
| func (d *ProgressDialog) Focus(delegate func(tview.Primitive)) {} | |||
| func (d *ProgressDialog) HasFocus() bool { | |||
| return d.Box.HasFocus() | |||
| } | |||
| func (d *ProgressDialog) InputHandler() func(*tcell.EventKey, func(tview.Primitive)) { | |||
| return d.WrapInputHandler(func(e *tcell.EventKey, setFocus func(tview.Primitive)) { | |||
| d.logger.Sugar().Debugf("progress dialog event %v received", e) | |||
| }) | |||
| } | |||
| func (d *ProgressDialog) tickStr(max int) string { | |||
| barColor := style.GetColorHex(style.PrgBarColor) | |||
| counter := d.counterValue | |||
| if counter < max-4 { | |||
| d.counterValue++ | |||
| } else { | |||
| d.counterValue = 0 | |||
| } | |||
| prgHeadStr := "" | |||
| hWidth := 0 | |||
| prgEndStr := "" | |||
| prgStr := "" | |||
| for i := 0; i < d.counterValue; i++ { | |||
| prgHeadStr += fmt.Sprintf("[black::]%s", prgCell) | |||
| hWidth++ | |||
| } | |||
| prgStr = strings.Repeat(prgCell, 4) | |||
| for i := 0; i < max+hWidth; i++ { | |||
| prgEndStr += fmt.Sprintf("[black::]%s", prgCell) | |||
| } | |||
| progress := fmt.Sprintf("%s[%s::]%s%s", prgHeadStr, barColor, prgStr, prgEndStr) | |||
| return progress | |||
| } | |||
| @ -0,0 +1,42 @@ | |||
| package dialogs | |||
| import ( | |||
| "strings" | |||
| "github.com/rivo/tview" | |||
| ) | |||
| const ( | |||
| DialogFormHeight = 3 | |||
| DialogMinWidth = 40 | |||
| DialogPadding = 3 | |||
| TableHeightOffset = 3 | |||
| ) | |||
| type Dialog interface { | |||
| tview.Primitive | |||
| Display() | |||
| Hide() | |||
| IsDisplay() bool | |||
| } | |||
| func getMessageWidth(message string) int { | |||
| var messageWidth int | |||
| for _, msg := range strings.Split(message, "\n") { | |||
| if len(msg) > messageWidth { | |||
| messageWidth = len(msg) | |||
| } | |||
| } | |||
| return messageWidth | |||
| } | |||
| func CheckDialogFocus(dialogs ...Dialog) bool { | |||
| for _, dia := range dialogs { | |||
| if dia.HasFocus() { | |||
| return true | |||
| } | |||
| } | |||
| return false | |||
| } | |||
| @ -0,0 +1,112 @@ | |||
| package help | |||
| import ( | |||
| "fmt" | |||
| "git.brettb.xyz/goinv/client/internal/ui/style" | |||
| "git.brettb.xyz/goinv/client/internal/ui/utils" | |||
| "github.com/gdamore/tcell/v2" | |||
| "github.com/rivo/tview" | |||
| ) | |||
| type Help struct { | |||
| *tview.Box | |||
| title string | |||
| layout *tview.Flex | |||
| } | |||
| func NewHelp(appName string, appVersion string) *Help { | |||
| help := &Help{ | |||
| Box: tview.NewBox(), | |||
| title: "help", | |||
| } | |||
| headerColor := style.HelpHeaderFgColor | |||
| fgColor := style.FgColor | |||
| bgColor := style.BgColor | |||
| borderColor := style.BorderColor | |||
| keyinfo := tview.NewTable() | |||
| keyinfo.SetBackgroundColor(bgColor) | |||
| keyinfo.SetFixed(1, 1) | |||
| keyinfo.SetSelectable(false, false) | |||
| appinfo := tview.NewTextView(). | |||
| SetDynamicColors(true). | |||
| SetWrap(true). | |||
| SetTextAlign(tview.AlignLeft) | |||
| appinfo.SetBackgroundColor(bgColor) | |||
| appInfoText := fmt.Sprintf("%s %s - (C) 2024 Brett Bender", appName, appVersion) | |||
| appinfo.SetText(appInfoText) | |||
| appinfo.SetTextColor(headerColor) | |||
| rowIndex := 0 | |||
| colIndex := 0 | |||
| needInit := true | |||
| maxRowIndex := len(utils.UIKeyBindings) / 2 | |||
| for i := 0; i < len(utils.UIKeyBindings); i++ { | |||
| if i >= maxRowIndex { | |||
| if needInit { | |||
| colIndex = 2 | |||
| rowIndex = 0 | |||
| needInit = false | |||
| } | |||
| } | |||
| keyinfo.SetCell(rowIndex, colIndex, | |||
| tview.NewTableCell(fmt.Sprintf("%s:", utils.UIKeyBindings[i].Label())). | |||
| SetAlign(tview.AlignRight). | |||
| SetBackgroundColor(bgColor). | |||
| SetSelectable(true).SetTextColor(headerColor)) | |||
| keyinfo.SetCell(rowIndex, colIndex+1, | |||
| tview.NewTableCell(utils.UIKeyBindings[i].Description()). | |||
| SetAlign(tview.AlignLeft). | |||
| SetBackgroundColor(bgColor). | |||
| SetSelectable(true). | |||
| SetTextColor(fgColor)) | |||
| rowIndex++ | |||
| } | |||
| mlayout := tview.NewFlex().SetDirection(tview.FlexRow) | |||
| mlayout.AddItem(appinfo, 1, 0, false) | |||
| mlayout.AddItem(utils.EmptyBoxSpace(bgColor), 1, 0, false) | |||
| mlayout.AddItem(keyinfo, 0, 1, false) | |||
| mlayout.AddItem(utils.EmptyBoxSpace(bgColor), 1, 0, false) | |||
| help.layout = tview.NewFlex().SetDirection(tview.FlexColumn) | |||
| help.layout.AddItem(utils.EmptyBoxSpace(bgColor), 1, 0, false) | |||
| help.layout.AddItem(mlayout, 0, 1, false) | |||
| help.layout.AddItem(utils.EmptyBoxSpace(bgColor), 1, 0, false) | |||
| help.layout.SetBorder(true) | |||
| help.layout.SetBackgroundColor(bgColor) | |||
| help.layout.SetBorderColor(borderColor) | |||
| return help | |||
| } | |||
| func (help *Help) GetTitle() string { | |||
| return help.title | |||
| } | |||
| func (help *Help) HasFocus() bool { | |||
| return utils.CheckFocus(help.Box, help.layout) | |||
| } | |||
| func (help *Help) Focus(delegate func(tview.Primitive)) { | |||
| delegate(help.layout) | |||
| } | |||
| func (help *Help) Draw(screen tcell.Screen) { | |||
| x, y, width, height := help.Box.GetInnerRect() | |||
| if height <= 3 { | |||
| return | |||
| } | |||
| help.Box.DrawForSubclass(screen, help) | |||
| help.layout.SetRect(x, y, width, height) | |||
| help.layout.Draw(screen) | |||
| } | |||
| @ -0,0 +1,44 @@ | |||
| package style | |||
| import ( | |||
| "github.com/gdamore/tcell/v2" | |||
| "github.com/rivo/tview" | |||
| ) | |||
| var ( | |||
| // main views | |||
| FgColor = tview.Styles.PrimaryTextColor | |||
| BgColor = tview.Styles.PrimitiveBackgroundColor | |||
| BorderColor = tcell.ColorLightBlue | |||
| MenuFgColor = tcell.ColorBlack | |||
| MenuBgColor = tcell.ColorLightBlue | |||
| HelpHeaderFgColor = tcell.ColorLightBlue | |||
| PageHeaderBgColor = tcell.ColorPink | |||
| PageHeaderFgColor = tview.Styles.PrimaryTextColor | |||
| // dialogs | |||
| DialogBgColor = tcell.ColorLightBlue | |||
| DialogFgColor = tcell.ColorBlack | |||
| DialogBorderColor = tcell.ColorLightBlue | |||
| DialogSubBoxBgColor = tcell.ColorWhite | |||
| DialogSubBoxFgColor = tcell.ColorBlack | |||
| DialogSubBoxBorderColor = tcell.ColorLightBlue | |||
| ErrorDialogBgColor = tcell.ColorRed | |||
| ErrorDialogButtonBgColor = tcell.ColorPink | |||
| // tables | |||
| TableHeaderBgColor = tcell.ColorLightBlue | |||
| TableHeaderFgColor = tcell.ColorBlack | |||
| TableSelectedFgColor = tcell.ColorBlack | |||
| TableSelectedBgColor = tcell.ColorWhite | |||
| // progressbar | |||
| PrgBarColor = tcell.ColorGreen | |||
| PrgBarBgColor = tcell.ColorDarkGreen | |||
| // other | |||
| ButtonBgColor = tcell.ColorBlack | |||
| ButtonFgColor = tcell.ColorLightBlue | |||
| ButtonSelectedFgColor = tcell.ColorBlack | |||
| ButtonSelectedBgColor = tcell.ColorWhite | |||
| ) | |||
| @ -0,0 +1,14 @@ | |||
| //go:build !windows | |||
| // +build !windows | |||
| package style | |||
| import ( | |||
| "fmt" | |||
| "github.com/gdamore/tcell/v2" | |||
| ) | |||
| func GetColorHex(color tcell.Color) string { | |||
| return fmt.Sprintf("#%06x", color.Hex()) | |||
| } | |||
| @ -0,0 +1,15 @@ | |||
| //go:build windows | |||
| // +build windows | |||
| package style | |||
| import "github.com/gdamore/tcell/v2" | |||
| func GetColorHex(color tcell.Color) string { | |||
| for name, c := range tcell.ColorNames { | |||
| if c == color { | |||
| return name | |||
| } | |||
| } | |||
| return "" | |||
| } | |||
| @ -0,0 +1,147 @@ | |||
| package utils | |||
| import ( | |||
| "github.com/gdamore/tcell/v2" | |||
| "go.uber.org/zap" | |||
| ) | |||
| var ( | |||
| CommandMenuKey = uiKeyInfo{ | |||
| Key: tcell.Key(256), | |||
| KeyRune: 'm', | |||
| KeyLabel: "m", | |||
| KeyDesc: "display command menu", | |||
| } | |||
| NextScreenKey = uiKeyInfo{ | |||
| Key: tcell.Key(256), | |||
| KeyRune: 'l', | |||
| KeyLabel: "l", | |||
| KeyDesc: "switch to next screen", | |||
| } | |||
| PreviousScreenKey = uiKeyInfo{ | |||
| Key: tcell.Key(256), | |||
| KeyRune: 'h', | |||
| KeyLabel: "h", | |||
| KeyDesc: "switch to previous screen", | |||
| } | |||
| MoveUpKey = uiKeyInfo{ | |||
| Key: tcell.KeyUp, | |||
| KeyRune: 'k', | |||
| KeyLabel: "k", | |||
| KeyDesc: "move up", | |||
| } | |||
| MoveDownKey = uiKeyInfo{ | |||
| Key: tcell.KeyDown, | |||
| KeyRune: 'j', | |||
| KeyLabel: "j", | |||
| KeyDesc: "move down", | |||
| } | |||
| CloseDialogKey = uiKeyInfo{ | |||
| Key: tcell.KeyEsc, | |||
| KeyLabel: "Esc", | |||
| KeyDesc: "close the active dialog", | |||
| } | |||
| SwitchFocusKey = uiKeyInfo{ | |||
| Key: tcell.KeyTab, | |||
| KeyLabel: "Tab", | |||
| KeyDesc: "switch between widgets", | |||
| } | |||
| ArrowUpKey = uiKeyInfo{ | |||
| Key: tcell.KeyUp, | |||
| KeyLabel: "arrow up", | |||
| KeyDesc: "move up", | |||
| } | |||
| ArrowDownKey = uiKeyInfo{ | |||
| Key: tcell.KeyDown, | |||
| KeyLabel: "arrow down", | |||
| KeyDesc: "move down", | |||
| } | |||
| ArrowLeftKey = uiKeyInfo{ | |||
| Key: tcell.KeyLeft, | |||
| KeyLabel: "Arrow Left", | |||
| KeyDesc: "previous screen", | |||
| } | |||
| ArrowRightKey = uiKeyInfo{ | |||
| Key: tcell.KeyRight, | |||
| KeyLabel: "Arrow Right", | |||
| KeyDesc: "next screen", | |||
| } | |||
| AppExitKey = uiKeyInfo{ | |||
| Key: tcell.KeyCtrlC, | |||
| KeyLabel: "Ctrl+c", | |||
| KeyDesc: "exit application", | |||
| } | |||
| HelpScreenKey = uiKeyInfo{ | |||
| Key: tcell.KeyF1, | |||
| KeyLabel: "F1", | |||
| KeyDesc: "display help screen", | |||
| } | |||
| AssetsScreenKey = uiKeyInfo{ | |||
| Key: tcell.KeyF2, | |||
| KeyLabel: "F2", | |||
| KeyDesc: "display assets screen", | |||
| } | |||
| ) | |||
| var UIKeyBindings = []uiKeyInfo{ | |||
| CommandMenuKey, | |||
| NextScreenKey, | |||
| PreviousScreenKey, | |||
| MoveUpKey, | |||
| MoveDownKey, | |||
| CloseDialogKey, | |||
| SwitchFocusKey, | |||
| ArrowUpKey, | |||
| ArrowDownKey, | |||
| ArrowLeftKey, | |||
| ArrowRightKey, | |||
| AppExitKey, | |||
| HelpScreenKey, | |||
| AssetsScreenKey, | |||
| } | |||
| type uiKeyInfo struct { | |||
| Key tcell.Key | |||
| KeyRune rune | |||
| KeyLabel string | |||
| KeyDesc string | |||
| } | |||
| func (key *uiKeyInfo) Label() string { | |||
| return key.KeyLabel | |||
| } | |||
| func (key *uiKeyInfo) Rune() rune { | |||
| return key.KeyRune | |||
| } | |||
| func (key *uiKeyInfo) EventKey() tcell.Key { | |||
| return key.Key | |||
| } | |||
| func (key *uiKeyInfo) Description() string { | |||
| return key.KeyDesc | |||
| } | |||
| func ParseKeyEventKey(logger *zap.Logger, event *tcell.EventKey) *tcell.EventKey { | |||
| logger.Sugar().Debugw("parse key event", | |||
| "event", event, | |||
| "key", event.Key(), | |||
| "name", event.Name()) | |||
| switch event.Rune() { | |||
| case MoveUpKey.KeyRune: | |||
| return tcell.NewEventKey(MoveUpKey.Key, MoveUpKey.KeyRune, tcell.ModNone) | |||
| case MoveDownKey.KeyRune: | |||
| return tcell.NewEventKey(MoveDownKey.Key, MoveDownKey.KeyRune, tcell.ModNone) | |||
| } | |||
| switch event.Key() { | |||
| case ArrowLeftKey.Key: | |||
| return tcell.NewEventKey(PreviousScreenKey.Key, PreviousScreenKey.KeyRune, tcell.ModNone) | |||
| case ArrowRightKey.Key: | |||
| return tcell.NewEventKey(NextScreenKey.Key, NextScreenKey.KeyRune, tcell.ModNone) | |||
| } | |||
| return event | |||
| } | |||
| @ -0,0 +1,29 @@ | |||
| package utils | |||
| import ( | |||
| "time" | |||
| "github.com/gdamore/tcell/v2" | |||
| "github.com/rivo/tview" | |||
| ) | |||
| const ( | |||
| RefreshInterval = 250 * time.Millisecond | |||
| ) | |||
| func EmptyBoxSpace(bgColor tcell.Color) *tview.Box { | |||
| box := tview.NewBox() | |||
| box.SetBackgroundColor(bgColor) | |||
| box.SetBorder(false) | |||
| return box | |||
| } | |||
| func CheckFocus(prims ...tview.Primitive) bool { | |||
| for _, prim := range prims { | |||
| if prim.HasFocus() { | |||
| return true | |||
| } | |||
| } | |||
| return false | |||
| } | |||
| @ -0,0 +1,23 @@ | |||
| //go:build mage | |||
| // +build mage | |||
| package main | |||
| import ( | |||
| "github.com/magefile/mage/mg" | |||
| "github.com/magefile/mage/sh" | |||
| ) | |||
| var Default = Build | |||
| func Build() error { | |||
| if err := sh.Run("go", "mod", "download"); err != nil { | |||
| return err | |||
| } | |||
| return sh.Run("go", "build", "-o", "./build/goinv-client", "./cmd/client") | |||
| } | |||
| func Run() error { | |||
| mg.Deps(Build) | |||
| return sh.RunV("./build/goinv-client") | |||
| } | |||