| @ -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") | |||||
| } | |||||