From bae07cf046bfa54b39f518dfef8c50bd091bc1c4 Mon Sep 17 00:00:00 2001 From: Jacob Shufro Date: Fri, 19 Jun 2026 12:13:53 -0400 Subject: [PATCH] Make it so esc saves changes before exiting page when modifying text boxes --- rocketpool-cli/service/config/config-form.go | 211 ++++++------------ .../service/config/settings-metrics.go | 12 +- .../service/config/settings-smartnode.go | 20 +- .../service/config/standard-layout.go | 33 ++- 4 files changed, 99 insertions(+), 177 deletions(-) diff --git a/rocketpool-cli/service/config/config-form.go b/rocketpool-cli/service/config/config-form.go index 677221aa2..c23bd4d82 100644 --- a/rocketpool-cli/service/config/config-form.go +++ b/rocketpool-cli/service/config/config-form.go @@ -17,6 +17,42 @@ type parameterizedFormItem struct { item tview.FormItem } +func (pfi *parameterizedFormItem) commit() { + switch pfi.item.(type) { + case *tview.Checkbox: + pfi.parameter.Value = pfi.item.(*tview.Checkbox).IsChecked() + case *tview.InputField: + var err error + inputField := pfi.item.(*tview.InputField) + switch pfi.parameter.Type { + case cfgtypes.ParameterType_Int: + pfi.parameter.Value, err = strconv.ParseInt(inputField.GetText(), 0, 0) + if err != nil { + // TODO: show error modal? + inputField.SetText("") + } + case cfgtypes.ParameterType_Uint: + pfi.parameter.Value, err = strconv.ParseUint(inputField.GetText(), 0, 0) + if err != nil { + // TODO: show error modal? + inputField.SetText("") + } + case cfgtypes.ParameterType_Uint16: + pfi.parameter.Value, err = strconv.ParseUint(inputField.GetText(), 0, 16) + if err != nil { + // TODO: show error modal? + inputField.SetText("") + } + case cfgtypes.ParameterType_String, cfgtypes.ParameterType_Float: + pfi.parameter.Value = strings.TrimSpace(inputField.GetText()) + default: + panic(fmt.Sprintf("Unknown parameter type for text field %v", pfi.parameter.Type)) + } + default: + panic(fmt.Sprintf("Unknown form item type %v", pfi.item)) + } +} + // Create a list of form items based on a set of parameters func createParameterizedFormItems(params []*cfgtypes.Parameter, descriptionBox *tview.TextView) []*parameterizedFormItem { formItems := []*parameterizedFormItem{} @@ -25,18 +61,12 @@ func createParameterizedFormItems(params []*cfgtypes.Parameter, descriptionBox * switch param.Type { case cfgtypes.ParameterType_Bool: item = createParameterizedCheckbox(param) - case cfgtypes.ParameterType_Int: + case cfgtypes.ParameterType_Int, cfgtypes.ParameterType_Uint, cfgtypes.ParameterType_Uint16: item = createParameterizedIntField(param) - case cfgtypes.ParameterType_Uint: - item = createParameterizedUintField(param) - case cfgtypes.ParameterType_Uint16: - item = createParameterizedUint16Field(param) - case cfgtypes.ParameterType_String: + case cfgtypes.ParameterType_String, cfgtypes.ParameterType_Float: item = createParameterizedStringField(param) case cfgtypes.ParameterType_Choice: item = createParameterizedDropDown(param, descriptionBox) - case cfgtypes.ParameterType_Float: - item = createParameterizedStringField(param) default: panic(fmt.Sprintf("Unknown parameter type %v", param)) } @@ -50,142 +80,56 @@ func createParameterizedFormItems(params []*cfgtypes.Parameter, descriptionBox * func createParameterizedCheckbox(param *cfgtypes.Parameter) *parameterizedFormItem { item := tview.NewCheckbox(). SetLabel(param.Name). - SetChecked(param.Value == true). - SetChangedFunc(func(checked bool) { - param.Value = checked - }) - item.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { - switch event.Key() { - case tcell.KeyDown, tcell.KeyTab: - return tcell.NewEventKey(tcell.KeyTab, 0, 0) - case tcell.KeyUp, tcell.KeyBacktab: - return tcell.NewEventKey(tcell.KeyBacktab, 0, 0) - default: - return event - } - }) - - return ¶meterizedFormItem{ + SetChecked(param.Value == true) + out := ¶meterizedFormItem{ parameter: param, item: item, } -} - -// Create a standard int field -func createParameterizedIntField(param *cfgtypes.Parameter) *parameterizedFormItem { - item := tview.NewInputField(). - SetLabel(param.Name). - SetAcceptanceFunc(tview.InputFieldInteger) - item.SetDoneFunc(func(key tcell.Key) { - if key == tcell.KeyEscape { - item.SetText("") - } else { - value, err := strconv.ParseInt(item.GetText(), 0, 0) - if err != nil { - // TODO: show error modal? - item.SetText("") - } else { - param.Value = int(value) - } - } - }) - item.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { - switch event.Key() { - case tcell.KeyDown, tcell.KeyTab: - return tcell.NewEventKey(tcell.KeyTab, 0, 0) - case tcell.KeyUp, tcell.KeyBacktab: - return tcell.NewEventKey(tcell.KeyBacktab, 0, 0) - default: - return event - } + item.SetInputCapture(navCapture) + item.SetChangedFunc(func(checked bool) { + out.commit() }) - return ¶meterizedFormItem{ - parameter: param, - item: item, + return out +} + +func navCapture(event *tcell.EventKey) *tcell.EventKey { + switch event.Key() { + case tcell.KeyDown, tcell.KeyTab: + return tcell.NewEventKey(tcell.KeyTab, 0, 0) + case tcell.KeyUp, tcell.KeyBacktab: + return tcell.NewEventKey(tcell.KeyBacktab, 0, 0) } + return event } -// Create a standard uint field -func createParameterizedUintField(param *cfgtypes.Parameter) *parameterizedFormItem { +// Create a standard int field +func createParameterizedIntField(param *cfgtypes.Parameter) *parameterizedFormItem { item := tview.NewInputField(). SetLabel(param.Name). SetAcceptanceFunc(tview.InputFieldInteger) - item.SetDoneFunc(func(key tcell.Key) { - if key == tcell.KeyEscape { - item.SetText("") - } else { - value, err := strconv.ParseUint(item.GetText(), 0, 0) - if err != nil { - // TODO: show error modal? - item.SetText("") - } else { - param.Value = int(value) - } - } - }) - item.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { - switch event.Key() { - case tcell.KeyDown, tcell.KeyTab: - return tcell.NewEventKey(tcell.KeyTab, 0, 0) - case tcell.KeyUp, tcell.KeyBacktab: - return tcell.NewEventKey(tcell.KeyBacktab, 0, 0) - default: - return event - } - }) - - return ¶meterizedFormItem{ + out := ¶meterizedFormItem{ parameter: param, item: item, } -} - -// Create a standard uint16 field -func createParameterizedUint16Field(param *cfgtypes.Parameter) *parameterizedFormItem { - item := tview.NewInputField(). - SetLabel(param.Name). - SetAcceptanceFunc(tview.InputFieldInteger) + item.SetInputCapture(navCapture) item.SetDoneFunc(func(key tcell.Key) { - if key == tcell.KeyEscape { - item.SetText("") - } else { - value, err := strconv.ParseUint(item.GetText(), 0, 16) - if err != nil { - // TODO: show error modal? - item.SetText("") - } else { - param.Value = int(value) - } - } - }) - item.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { - switch event.Key() { - case tcell.KeyDown, tcell.KeyTab: - return tcell.NewEventKey(tcell.KeyTab, 0, 0) - case tcell.KeyUp, tcell.KeyBacktab: - return tcell.NewEventKey(tcell.KeyBacktab, 0, 0) - default: - return event - } + out.commit() }) - return ¶meterizedFormItem{ - parameter: param, - item: item, - } + return out } // Create a standard string field func createParameterizedStringField(param *cfgtypes.Parameter) *parameterizedFormItem { item := tview.NewInputField(). SetLabel(param.Name) + out := ¶meterizedFormItem{ + parameter: param, + item: item, + } item.SetDoneFunc(func(key tcell.Key) { - if key == tcell.KeyEscape { - item.SetText("") - } else { - param.Value = strings.TrimSpace(item.GetText()) - } + out.commit() }) item.SetAcceptanceFunc(func(textToCheck string, lastChar rune) bool { if param.MaxLength > 0 { @@ -196,21 +140,9 @@ func createParameterizedStringField(param *cfgtypes.Parameter) *parameterizedFor // TODO: regex support return true }) - item.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { - switch event.Key() { - case tcell.KeyDown, tcell.KeyTab: - return tcell.NewEventKey(tcell.KeyTab, 0, 0) - case tcell.KeyUp, tcell.KeyBacktab: - return tcell.NewEventKey(tcell.KeyBacktab, 0, 0) - default: - return event - } - }) + item.SetInputCapture(navCapture) - return ¶meterizedFormItem{ - parameter: param, - item: item, - } + return out } // Create a standard choice field @@ -233,16 +165,7 @@ func createParameterizedDropDown(param *cfgtypes.Parameter, descriptionBox *tvie descriptionBox.SetText(descriptions[index]) }) item.SetTextOptions(" ", " ", "", "", "") - item.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { - switch event.Key() { - case tcell.KeyDown, tcell.KeyTab: - return tcell.NewEventKey(tcell.KeyTab, 0, 0) - case tcell.KeyUp, tcell.KeyBacktab: - return tcell.NewEventKey(tcell.KeyBacktab, 0, 0) - default: - return event - } - }) + item.SetInputCapture(navCapture) list := item.GetList() list.SetSelectedBackgroundColor(tcell.Color46) list.SetSelectedTextColor(tcell.ColorBlack) diff --git a/rocketpool-cli/service/config/settings-metrics.go b/rocketpool-cli/service/config/settings-metrics.go index 6f20177fc..e7dcc9ec1 100644 --- a/rocketpool-cli/service/config/settings-metrics.go +++ b/rocketpool-cli/service/config/settings-metrics.go @@ -65,12 +65,12 @@ func (configPage *MetricsConfigPage) createContent() { // Set up the form items configPage.enableMetricsBox = createParameterizedCheckbox(&configPage.masterConfig.EnableMetrics) configPage.enableOdaoMetricsBox = createParameterizedCheckbox(&configPage.masterConfig.EnableODaoMetrics) - configPage.ecMetricsPortBox = createParameterizedUint16Field(&configPage.masterConfig.EcMetricsPort) - configPage.bnMetricsPortBox = createParameterizedUint16Field(&configPage.masterConfig.BnMetricsPort) - configPage.vcMetricsPortBox = createParameterizedUint16Field(&configPage.masterConfig.VcMetricsPort) - configPage.nodeMetricsPortBox = createParameterizedUint16Field(&configPage.masterConfig.NodeMetricsPort) - configPage.exporterMetricsPortBox = createParameterizedUint16Field(&configPage.masterConfig.ExporterMetricsPort) - configPage.watchtowerMetricsPortBox = createParameterizedUint16Field(&configPage.masterConfig.WatchtowerMetricsPort) + configPage.ecMetricsPortBox = createParameterizedIntField(&configPage.masterConfig.EcMetricsPort) + configPage.bnMetricsPortBox = createParameterizedIntField(&configPage.masterConfig.BnMetricsPort) + configPage.vcMetricsPortBox = createParameterizedIntField(&configPage.masterConfig.VcMetricsPort) + configPage.nodeMetricsPortBox = createParameterizedIntField(&configPage.masterConfig.NodeMetricsPort) + configPage.exporterMetricsPortBox = createParameterizedIntField(&configPage.masterConfig.ExporterMetricsPort) + configPage.watchtowerMetricsPortBox = createParameterizedIntField(&configPage.masterConfig.WatchtowerMetricsPort) configPage.grafanaItems = createParameterizedFormItems(configPage.masterConfig.Grafana.GetParameters(), configPage.layout.descriptionBox) configPage.prometheusItems = createParameterizedFormItems(configPage.masterConfig.Prometheus.GetParameters(), configPage.layout.descriptionBox) configPage.exporterItems = createParameterizedFormItems(configPage.masterConfig.Exporter.GetParameters(), configPage.layout.descriptionBox) diff --git a/rocketpool-cli/service/config/settings-smartnode.go b/rocketpool-cli/service/config/settings-smartnode.go index 62de274f3..131df5bcb 100644 --- a/rocketpool-cli/service/config/settings-smartnode.go +++ b/rocketpool-cli/service/config/settings-smartnode.go @@ -1,8 +1,6 @@ package config import ( - "github.com/gdamore/tcell/v2" - "github.com/rocket-pool/smartnode/shared/services/config" cfgtypes "github.com/rocket-pool/smartnode/shared/types/config" ) @@ -49,23 +47,7 @@ func (configPage *SmartnodeConfigPage) createContent() { layout.createForm(&masterConfig.Smartnode.Network, "Smart Node and TX Fee Settings") // Return to the home page after pressing Escape - layout.form.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { - if event.Key() == tcell.KeyEsc { - // Close all dropdowns and break if one was open - for _, param := range configPage.layout.parameters { - dropDown, ok := param.item.(*DropDown) - if ok && dropDown.open { - dropDown.CloseList(configPage.home.md.app) - return nil - } - } - - // Return to the home page - configPage.home.md.setPage(configPage.home.homePage) - return nil - } - return event - }) + layout.form.SetInputCapture(layout.getInputCapture(configPage.home.md, configPage.home.homePage)) // Set up the form items params := append(masterConfig.Smartnode.GetParameters(), &masterConfig.EnableIPv6, &masterConfig.Alertmanager.ShowAlertsOnCLI) diff --git a/rocketpool-cli/service/config/standard-layout.go b/rocketpool-cli/service/config/standard-layout.go index 84c149770..50eb0004f 100644 --- a/rocketpool-cli/service/config/standard-layout.go +++ b/rocketpool-cli/service/config/standard-layout.go @@ -210,22 +210,39 @@ func (layout *standardLayout) mapParameterizedFormItems(params ...*parameterized } } -// Sets up a handler to return to the specified homePage when the user presses escape on the layout. -func (layout *standardLayout) setupEscapeReturnHomeHandler(md *MainDisplay, homePage *page) { - layout.grid.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { - // Return to the home page +func (layout *standardLayout) getInputCapture(md *MainDisplay, prev *page) func(event *tcell.EventKey) *tcell.EventKey { + return func(event *tcell.EventKey) *tcell.EventKey { if event.Key() == tcell.KeyEsc { // Close all dropdowns and break if one was open + // Save the current modifications to text parameters for _, param := range layout.parameters { - dropDown, ok := param.item.(*DropDown) - if ok && dropDown.open { + formItem := param.item + if !formItem.HasFocus() { + continue + } + + // Close the dropdown if this field is one and it is open + if dropDown, ok := param.item.(*DropDown); ok && dropDown.open { dropDown.CloseList(md.app) return nil } + + // Save the text if this field is one + if _, ok := param.item.(*tview.InputField); ok { + param.commit() + // Exit the loop to return to the home page + break + } } - md.setPage(homePage) + + md.setPage(prev) return nil } return event - }) + } +} + +// Sets up a handler to return to the specified homePage when the user presses escape on the layout. +func (layout *standardLayout) setupEscapeReturnHomeHandler(md *MainDisplay, homePage *page) { + layout.grid.SetInputCapture(layout.getInputCapture(md, homePage)) }