initial commit
This commit is contained in:
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
|
||||
}
|
||||
Reference in New Issue
Block a user