package astdialogs import ( "fmt" "strconv" "strings" "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 ( assetCreateDialogMaxWidth = 100 assetCreateDialogHeight = 17 ) const ( createAssetFormFocus = 0 + iota createCategoriesFocus createCategoryPagesFocus createAssetNameFieldFocus createAssetQuantityFieldFocus createAssetLengthFieldFocus createAssetManufacturerFieldFocus createAssetModelFieldFocus createAssetPriceFieldFocus createAssetCategoryFieldFocus createAssetShelfFieldFocus createAssetCommentFieldFocus ) const ( generalPageIndex = 0 + iota locationPageIndex commentPageIndex ) type AssetCreateDialog struct { *tview.Box layout *tview.Flex createCategoryLabels []string categories *tview.TextView categoryPages *tview.Pages generalInfoPage *tview.Flex locationPage *tview.Flex commentsPage *tview.Flex form *tview.Form display bool activePageIndex int focusElement int logger *zap.Logger client *api.APIClient categoryList []*types.Category shelfList []*types.ShelfLocation assetNameField *tview.InputField assetQuantityField *tview.InputField assetLengthField *tview.InputField assetManufacturerField *tview.InputField assetModelField *tview.InputField assetPriceField *tview.InputField assetCategoryField *tview.DropDown assetShelfField *tview.DropDown assetCommentsArea *tview.TextArea focusMap map[int]tview.Primitive cancelHandler func() createHandler func() } func NewAssetCreateDialog(logger *zap.Logger, client *api.APIClient) *AssetCreateDialog { createDialog := AssetCreateDialog{ Box: tview.NewBox(), layout: tview.NewFlex(), createCategoryLabels: []string{ "General", "Location", "Comments", }, categories: tview.NewTextView(), categoryPages: tview.NewPages(), generalInfoPage: tview.NewFlex(), locationPage: tview.NewFlex(), commentsPage: tview.NewFlex(), form: tview.NewForm(), display: false, activePageIndex: 0, logger: logger, client: client, assetNameField: tview.NewInputField(), assetQuantityField: tview.NewInputField(), assetLengthField: tview.NewInputField(), assetManufacturerField: tview.NewInputField(), assetModelField: tview.NewInputField(), assetPriceField: tview.NewInputField(), assetCategoryField: tview.NewDropDown(), assetShelfField: tview.NewDropDown(), assetCommentsArea: tview.NewTextArea(), } createDialog.focusMap = map[int]tview.Primitive{ createAssetNameFieldFocus: createDialog.assetNameField, createAssetQuantityFieldFocus: createDialog.assetQuantityField, createAssetLengthFieldFocus: createDialog.assetLengthField, createAssetManufacturerFieldFocus: createDialog.assetManufacturerField, createAssetModelFieldFocus: createDialog.assetModelField, createAssetPriceFieldFocus: createDialog.assetPriceField, createAssetCategoryFieldFocus: createDialog.assetCategoryField, createAssetShelfFieldFocus: createDialog.assetShelfField, createAssetCommentFieldFocus: createDialog.assetCommentsArea, } createDialog.setupLayout() createDialog.setActiveCategory(0) createDialog.initCustomInputHandlers() return &createDialog } func (d *AssetCreateDialog) Display() { d.display = true d.initData() d.focusElement = createCategoryPagesFocus } func (d *AssetCreateDialog) IsDisplay() bool { return d.display } func (d *AssetCreateDialog) Hide() { d.display = false } func (d *AssetCreateDialog) HasFocus() bool { return utils.CheckFocus(d.categories, d.categoryPages, d.Box, d.form) } func (d *AssetCreateDialog) Focus(delegate func(tview.Primitive)) { switch d.focusElement { case createAssetFormFocus: button := d.form.GetButton(d.form.GetButtonCount() - 1) button.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { if event.Key() == utils.SwitchFocusKey.EventKey() { d.focusElement = createCategoriesFocus d.Focus(delegate) d.form.SetFocus(0) return nil } if event.Key() == tcell.KeyEnter { return nil } return event }) delegate(d.form) case createCategoriesFocus: d.categories.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { if event.Key() == utils.SwitchFocusKey.EventKey() { d.focusElement = createCategoryPagesFocus d.Focus(delegate) return nil } if event.Key() == tcell.KeyBacktab { d.focusElement = createAssetFormFocus d.Focus(delegate) return nil } event = utils.ParseKeyEventKey(d.logger, event) if event.Key() == tcell.KeyDown { d.nextCategory() } if event.Key() == tcell.KeyUp { d.previousCategory() } return event }) delegate(d.categories) case createCategoryPagesFocus: delegate(d.categoryPages) default: delegate(d.focusMap[d.focusElement]) } } func (d *AssetCreateDialog) InputHandler() func(*tcell.EventKey, func(tview.Primitive)) { return d.WrapInputHandler(func(event *tcell.EventKey, setFocus func(tview.Primitive)) { d.logger.Sugar().Debugf("asset create dialog event %v received", event) if event.Key() == utils.CloseDialogKey.EventKey() && !d.dropdownHasFocus() { d.cancelHandler() return } if d.generalInfoPage.HasFocus() { if handler := d.generalInfoPage.InputHandler(); handler != nil { if event.Key() == utils.SwitchFocusKey.EventKey() { d.setGeneralInfoNextFocus() } if event.Key() == tcell.KeyBacktab { d.setGeneralInfoPrevFocus() } handler(event, setFocus) return } } if d.locationPage.HasFocus() { if handler := d.locationPage.InputHandler(); handler != nil { if event.Key() == utils.SwitchFocusKey.EventKey() { d.setLocationNextFocus() } if event.Key() == tcell.KeyBacktab { d.setLocationPrevFocus() } handler(event, setFocus) return } } if d.commentsPage.HasFocus() { if handler := d.commentsPage.InputHandler(); handler != nil { if event.Key() == utils.SwitchFocusKey.EventKey() { d.setCommentsNextFocus() } if event.Key() == tcell.KeyBacktab { d.setCommentsPrevFocus() } handler(event, setFocus) return } } if d.categories.HasFocus() { if categoryHandler := d.categories.InputHandler(); categoryHandler != nil { categoryHandler(event, setFocus) return } } if d.form.HasFocus() { if formHandler := d.form.InputHandler(); formHandler != nil { if event.Key() == tcell.KeyEnter { enterButton := d.form.GetButton(d.form.GetButtonCount() - 1) if enterButton.HasFocus() { d.createHandler() } } formHandler(event, setFocus) return } } }) } func (d *AssetCreateDialog) SetRect(x, y, width, height int) { if width > assetCreateDialogMaxWidth { emptySpace := (width - assetCreateDialogMaxWidth) / 2 x += emptySpace width = assetCreateDialogMaxWidth } if height > assetCreateDialogHeight { emptySpace := (height - assetCreateDialogHeight) / 2 y += emptySpace height = assetCreateDialogHeight } d.Box.SetRect(x, y, width, height) } func (d *AssetCreateDialog) 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 *AssetCreateDialog) SetCreateFunc(f func()) *AssetCreateDialog { d.createHandler = f enterButton := d.form.GetButton(d.form.GetButtonCount() - 1) enterButton.SetSelectedFunc(f) return d } func (d *AssetCreateDialog) SetCancelFunc(f func()) *AssetCreateDialog { d.cancelHandler = f cancelButton := d.form.GetButton(d.form.GetButtonCount() - 2) cancelButton.SetSelectedFunc(f) return d } func (d *AssetCreateDialog) dropdownHasFocus() bool { return utils.CheckFocus(d.assetCategoryField, d.assetShelfField) } func (d *AssetCreateDialog) initData() { // Get available shelves shelfList, _ := d.client.RetrieveAllShelves() d.shelfList = shelfList shelfOptions := []string{""} for _, shelf := range d.shelfList { shelfOptions = append(shelfOptions, fmt.Sprintf("%s", shelf.Name)) // In case we want to do custom formatting later } // Get available categories categoryList, _ := d.client.RetrieveAllCategories() d.categoryList = categoryList categoryOptions := []string{""} for _, category := range d.categoryList { categoryOptions = append(categoryOptions, fmt.Sprintf("%s", category.Name)) } d.setActiveCategory(0) // General category d.assetNameField.SetText("") d.assetQuantityField.SetText("") d.assetLengthField.SetText("") d.assetManufacturerField.SetText("") d.assetModelField.SetText("") d.assetPriceField.SetText("") // Location category d.assetShelfField.SetOptions(shelfOptions, nil) d.assetShelfField.SetCurrentOption(0) d.assetCategoryField.SetOptions(categoryOptions, nil) d.assetCategoryField.SetCurrentOption(0) // Comments category d.assetCommentsArea.SetText("", true) } func (d *AssetCreateDialog) setupLayout() { bgColor := style.DialogBgColor fgColor := style.DialogFgColor d.categories.SetDynamicColors(true). SetWrap(true). SetTextAlign(tview.AlignLeft) d.categories.SetBackgroundColor(fgColor) d.categories.SetTextColor(bgColor) d.categories.SetBorder(true) d.categories.SetBorderColor(style.DialogSubBoxBorderColor) d.categoryPages.SetBackgroundColor(bgColor) d.categoryPages.SetBorder(true) d.categoryPages.SetBorderColor(style.DialogSubBoxBorderColor) d.setupGeneralInfoPageUI() d.setupLocationPageUI() d.setupCommentsPageUI() activatedStyle := tcell.StyleDefault. Background(style.ButtonSelectedBgColor). Foreground(style.ButtonSelectedFgColor) d.form.SetBackgroundColor(bgColor) d.form.AddButton("Cancel", nil) d.form.AddButton("Create", nil) d.form.SetButtonsAlign(tview.AlignRight) d.form.SetButtonBackgroundColor(style.ButtonBgColor) d.form.SetButtonTextColor(style.ButtonFgColor) d.form.SetButtonActivatedStyle(activatedStyle) d.categoryPages.AddPage(d.createCategoryLabels[generalPageIndex], d.generalInfoPage, true, true) d.categoryPages.AddPage(d.createCategoryLabels[locationPageIndex], d.locationPage, true, true) d.categoryPages.AddPage(d.createCategoryLabels[commentPageIndex], d.commentsPage, true, true) d.layout.SetBackgroundColor(bgColor) d.layout.SetBorder(true) d.layout.SetBorderColor(style.DialogBorderColor) d.layout.SetTitle("ASSET CREATE") d.layout.SetTitleColor(style.DialogFgColor) _, layoutWidth := utils.AlignStringListWidth(d.createCategoryLabels) layout := tview.NewFlex().SetDirection(tview.FlexColumn) layout.AddItem(d.categories, layoutWidth+6, 0, true) layout.AddItem(d.categoryPages, 0, 1, true) layout.SetBackgroundColor(bgColor) d.layout.SetDirection(tview.FlexRow) d.layout.AddItem(layout, 0, 1, true) d.layout.AddItem(d.form, dialogs.DialogFormHeight, 0, true) } func (d *AssetCreateDialog) setupGeneralInfoPageUI() { bgColor := style.DialogBgColor inputFieldBgColor := style.InputFieldBgColor pageLabelWidth := 14 d.assetNameField.SetLabel("name:") d.assetNameField.SetLabelWidth(pageLabelWidth) d.assetNameField.SetLabelColor(bgColor) d.assetNameField.SetBackgroundColor(style.DialogFgColor) d.assetNameField.SetFieldBackgroundColor(inputFieldBgColor) d.assetQuantityField.SetLabel("quantity:") d.assetQuantityField.SetLabelWidth(pageLabelWidth) d.assetQuantityField.SetLabelColor(bgColor) d.assetQuantityField.SetBackgroundColor(style.DialogFgColor) d.assetQuantityField.SetFieldBackgroundColor(inputFieldBgColor) d.assetLengthField.SetLabel("length:") d.assetLengthField.SetLabelWidth(pageLabelWidth) d.assetLengthField.SetLabelColor(bgColor) d.assetLengthField.SetBackgroundColor(style.DialogFgColor) d.assetLengthField.SetFieldBackgroundColor(inputFieldBgColor) d.assetManufacturerField.SetLabel("manufacturer:") d.assetManufacturerField.SetLabelWidth(pageLabelWidth) d.assetManufacturerField.SetLabelColor(bgColor) d.assetManufacturerField.SetBackgroundColor(style.DialogFgColor) d.assetManufacturerField.SetFieldBackgroundColor(inputFieldBgColor) d.assetModelField.SetLabel("model:") d.assetModelField.SetLabelWidth(pageLabelWidth) d.assetModelField.SetLabelColor(bgColor) d.assetModelField.SetBackgroundColor(style.DialogFgColor) d.assetModelField.SetFieldBackgroundColor(inputFieldBgColor) d.assetPriceField.SetLabel("price:") d.assetPriceField.SetLabelWidth(pageLabelWidth) d.assetPriceField.SetLabelColor(bgColor) d.assetPriceField.SetBackgroundColor(style.DialogFgColor) d.assetPriceField.SetFieldBackgroundColor(inputFieldBgColor) d.generalInfoPage.SetDirection(tview.FlexRow) d.generalInfoPage.AddItem(d.assetNameField, 1, 0, true) d.generalInfoPage.AddItem(utils.EmptyBoxSpace(bgColor), 1, 0, true) d.generalInfoPage.AddItem(d.assetQuantityField, 1, 0, true) d.generalInfoPage.AddItem(utils.EmptyBoxSpace(bgColor), 1, 0, true) d.generalInfoPage.AddItem(d.assetLengthField, 1, 0, true) d.generalInfoPage.AddItem(utils.EmptyBoxSpace(bgColor), 1, 0, true) d.generalInfoPage.AddItem(d.assetManufacturerField, 1, 0, true) d.generalInfoPage.AddItem(utils.EmptyBoxSpace(bgColor), 1, 0, true) d.generalInfoPage.AddItem(d.assetModelField, 1, 0, true) d.generalInfoPage.AddItem(utils.EmptyBoxSpace(bgColor), 1, 0, true) d.generalInfoPage.AddItem(d.assetPriceField, 1, 0, true) d.generalInfoPage.AddItem(utils.EmptyBoxSpace(bgColor), 1, 0, true) } func (d *AssetCreateDialog) setupLocationPageUI() { bgColor := style.DialogBgColor inputFieldBgColor := style.InputFieldBgColor pageLabelWidth := 12 ddUnselectedStyle := style.DropdownUnselected ddSelectedStyle := style.DropdownSelected d.assetCategoryField.SetLabel("category:") d.assetCategoryField.SetLabelWidth(pageLabelWidth) d.assetCategoryField.SetBackgroundColor(bgColor) d.assetCategoryField.SetLabelColor(style.DialogFgColor) d.assetCategoryField.SetListStyles(ddUnselectedStyle, ddSelectedStyle) d.assetCategoryField.SetFieldBackgroundColor(inputFieldBgColor) d.assetShelfField.SetLabel("shelf:") d.assetShelfField.SetLabelWidth(pageLabelWidth) d.assetShelfField.SetBackgroundColor(bgColor) d.assetShelfField.SetLabelColor(style.DialogFgColor) d.assetShelfField.SetListStyles(ddUnselectedStyle, ddSelectedStyle) d.assetShelfField.SetFieldBackgroundColor(inputFieldBgColor) d.locationPage.SetDirection(tview.FlexRow) d.locationPage.AddItem(d.assetCategoryField, 1, 0, true) d.locationPage.AddItem(utils.EmptyBoxSpace(bgColor), 1, 0, true) d.locationPage.AddItem(d.assetShelfField, 1, 0, true) d.locationPage.SetBackgroundColor(bgColor) } func (d *AssetCreateDialog) setupCommentsPageUI() { bgColor := style.DialogBgColor inputFieldBgColor := style.InputFieldBgColor pageLabelWidth := 12 d.assetCommentsArea.SetLabel("comments:") d.assetCommentsArea.SetLabelWidth(pageLabelWidth) d.assetCommentsArea.SetBackgroundColor(inputFieldBgColor) d.assetCommentsArea.SetLabelStyle(tcell.StyleDefault.Foreground(style.DialogFgColor).Background(bgColor)) d.assetCommentsArea.SetWrap(true) d.commentsPage.AddItem(d.assetCommentsArea, 0, 1, true) d.commentsPage.SetBackgroundColor(bgColor) } func (d *AssetCreateDialog) setActiveCategory(idx int) { fgColor := style.ButtonSelectedFgColor bgBolor := style.ButtonSelectedBgColor ctgTextColor := style.GetColorHex(fgColor) ctgBgColor := style.GetColorHex(bgBolor) d.activePageIndex = idx d.categories.Clear() alignedList, _ := utils.AlignStringListWidth(d.createCategoryLabels) var ctgList []string for i, lbl := range alignedList { if i == idx { ctgList = append(ctgList, fmt.Sprintf("[%s:%s:b]-> %s ", ctgTextColor, ctgBgColor, lbl)) continue } ctgList = append(ctgList, fmt.Sprintf("[-:-:-] %s ", lbl)) } d.categories.SetText(strings.Join(ctgList, "\n")) d.categoryPages.SwitchToPage(d.createCategoryLabels[idx]) } func (d *AssetCreateDialog) nextCategory() { activePage := d.activePageIndex if d.activePageIndex < len(d.createCategoryLabels)-1 { activePage++ d.setActiveCategory(activePage) return } d.setActiveCategory(0) } func (d *AssetCreateDialog) previousCategory() { activePage := d.activePageIndex if d.activePageIndex > 0 { activePage-- d.setActiveCategory(activePage) return } d.setActiveCategory(len(d.createCategoryLabels) - 1) } func (d *AssetCreateDialog) initCustomInputHandlers() { d.assetShelfField.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { event = utils.ParseKeyEventKey(d.logger, event) return event }) d.assetCategoryField.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { event = utils.ParseKeyEventKey(d.logger, event) return event }) } func (d *AssetCreateDialog) setGeneralInfoNextFocus() { // Name -> Quantity -> Length -> Manufacturer -> Model -> Price if d.assetNameField.HasFocus() { d.focusElement = createAssetQuantityFieldFocus return } if d.assetQuantityField.HasFocus() { d.focusElement = createAssetLengthFieldFocus return } if d.assetLengthField.HasFocus() { d.focusElement = createAssetManufacturerFieldFocus return } if d.assetManufacturerField.HasFocus() { d.focusElement = createAssetModelFieldFocus return } if d.assetModelField.HasFocus() { d.focusElement = createAssetPriceFieldFocus return } d.focusElement = createAssetFormFocus } func (d *AssetCreateDialog) setGeneralInfoPrevFocus() { if d.assetNameField.HasFocus() { d.focusElement = createCategoriesFocus return } if d.assetQuantityField.HasFocus() { d.focusElement = createAssetNameFieldFocus return } if d.assetLengthField.HasFocus() { d.focusElement = createAssetQuantityFieldFocus return } if d.assetManufacturerField.HasFocus() { d.focusElement = createAssetLengthFieldFocus return } if d.assetModelField.HasFocus() { d.focusElement = createAssetManufacturerFieldFocus return } if d.assetPriceField.HasFocus() { d.focusElement = createAssetModelFieldFocus return } d.focusElement = createAssetPriceFieldFocus } func (d *AssetCreateDialog) setLocationNextFocus() { // Category -> Shelf if d.assetCategoryField.HasFocus() { d.focusElement = createAssetShelfFieldFocus return } d.focusElement = createAssetFormFocus } func (d *AssetCreateDialog) setLocationPrevFocus() { if d.assetCategoryField.HasFocus() { d.focusElement = createCategoriesFocus return } if d.assetShelfField.HasFocus() { d.focusElement = createAssetCategoryFieldFocus return } d.focusElement = createAssetShelfFieldFocus } func (d *AssetCreateDialog) setCommentsNextFocus() { d.focusElement = createAssetFormFocus } func (d *AssetCreateDialog) setCommentsPrevFocus() { d.focusElement = createCategoriesFocus } func (d *AssetCreateDialog) CreateAssetOptions() types.CreateAssetRequest { var ( quantity int categoryID *uint64 shelfID *uint64 price float64 ) var err error selectedCategoryIndex, _ := d.assetCategoryField.GetCurrentOption() if len(d.categoryList) > 0 && selectedCategoryIndex > 0 { categoryID = &d.categoryList[selectedCategoryIndex-1].ID } selectedShelfIndex, _ := d.assetShelfField.GetCurrentOption() if len(d.shelfList) > 0 && selectedShelfIndex > 0 { shelfID = &d.shelfList[selectedShelfIndex-1].ID } quantityStr := d.assetQuantityField.GetText() quantity, err = strconv.Atoi(quantityStr) if err != nil { quantity = 0 } priceStr := d.assetPriceField.GetText() price, err = strconv.ParseFloat(priceStr, 64) if err != nil { price = 0.0 } req := types.CreateAssetRequest{ Name: d.assetNameField.GetText(), Quantity: quantity, Length: d.assetLengthField.GetText(), Manufacturer: d.assetManufacturerField.GetText(), ModelName: d.assetModelField.GetText(), Price: price, Comments: d.assetCommentsArea.GetText(), ShelfLocationID: shelfID, CategoryID: categoryID, } return req }