initial commit
This commit is contained in:
117
.gitignore
vendored
Normal file
117
.gitignore
vendored
Normal file
@@ -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
|
||||||
17
cmd/client/client.go
Normal file
17
cmd/client/client.go
Normal file
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
24
go.mod
Normal file
24
go.mod
Normal file
@@ -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
|
||||||
|
)
|
||||||
66
go.sum
Normal file
66
go.sum
Normal file
@@ -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=
|
||||||
146
internal/api/api.go
Normal file
146
internal/api/api.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
11
internal/api/utils.go
Normal file
11
internal/api/utils.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
112
internal/app/app.go
Normal file
112
internal/app/app.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
8
internal/app/init.go
Normal file
8
internal/app/init.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
func (app *App) initUI() {
|
||||||
|
|
||||||
|
// Assets page
|
||||||
|
app.assets.UpdateShelfData()
|
||||||
|
app.assets.UpdateAssetData()
|
||||||
|
}
|
||||||
44
internal/app/menu.go
Normal file
44
internal/app/menu.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
19
internal/app/refresh.go
Normal file
19
internal/app/refresh.go
Normal file
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
56
internal/app/screens.go
Normal file
56
internal/app/screens.go
Normal file
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
42
internal/types/assets.go
Normal file
42
internal/types/assets.go
Normal file
@@ -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"`
|
||||||
|
}
|
||||||
11
internal/types/buildings.go
Normal file
11
internal/types/buildings.go
Normal file
@@ -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"`
|
||||||
|
}
|
||||||
11
internal/types/categories.go
Normal file
11
internal/types/categories.go
Normal file
@@ -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"`
|
||||||
|
}
|
||||||
23
internal/types/shelves.go
Normal file
23
internal/types/shelves.go
Normal file
@@ -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"`
|
||||||
|
}
|
||||||
216
internal/ui/assets/assets.go
Normal file
216
internal/ui/assets/assets.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
103
internal/ui/assets/command.go
Normal file
103
internal/ui/assets/command.go
Normal file
@@ -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()
|
||||||
|
}
|
||||||
48
internal/ui/assets/data.go
Normal file
48
internal/ui/assets/data.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
24
internal/ui/assets/draw.go
Normal file
24
internal/ui/assets/draw.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
54
internal/ui/assets/key.go
Normal file
54
internal/ui/assets/key.go
Normal file
@@ -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)
|
||||||
|
})
|
||||||
|
}
|
||||||
97
internal/ui/assets/refresh.go
Normal file
97
internal/ui/assets/refresh.go
Normal file
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
300
internal/ui/dialogs/command.go
Normal file
300
internal/ui/dialogs/command.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
203
internal/ui/dialogs/confirm.go
Normal file
203
internal/ui/dialogs/confirm.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
106
internal/ui/dialogs/error.go
Normal file
106
internal/ui/dialogs/error.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
238
internal/ui/dialogs/message.go
Normal file
238
internal/ui/dialogs/message.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
128
internal/ui/dialogs/progress.go
Normal file
128
internal/ui/dialogs/progress.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
42
internal/ui/dialogs/utils.go
Normal file
42
internal/ui/dialogs/utils.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
112
internal/ui/help/help.go
Normal file
112
internal/ui/help/help.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
44
internal/ui/style/style.go
Normal file
44
internal/ui/style/style.go
Normal file
@@ -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
|
||||||
|
)
|
||||||
14
internal/ui/style/utils.go
Normal file
14
internal/ui/style/utils.go
Normal file
@@ -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())
|
||||||
|
}
|
||||||
15
internal/ui/style/utils_windows.go
Normal file
15
internal/ui/style/utils_windows.go
Normal file
@@ -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 ""
|
||||||
|
}
|
||||||
147
internal/ui/utils/keys.go
Normal file
147
internal/ui/utils/keys.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
29
internal/ui/utils/utils.go
Normal file
29
internal/ui/utils/utils.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
23
magefile.go
Normal file
23
magefile.go
Normal file
@@ -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")
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user