diff --git a/pkg/gui/context/menu_context.go b/pkg/gui/context/menu_context.go index a6b0e77cb..e4b26f884 100644 --- a/pkg/gui/context/menu_context.go +++ b/pkg/gui/context/menu_context.go @@ -50,6 +50,8 @@ func NewMenuContext( type MenuViewModel struct { c *ContextCommon menuItems []*types.MenuItem + prompt string + promptLines []string columnAlignment []utils.Alignment *FilteredListViewModel[*types.MenuItem] } @@ -73,6 +75,23 @@ func (self *MenuViewModel) SetMenuItems(items []*types.MenuItem, columnAlignment self.columnAlignment = columnAlignment } +func (self *MenuViewModel) GetPrompt() string { + return self.prompt +} + +func (self *MenuViewModel) SetPrompt(prompt string) { + self.prompt = prompt + self.promptLines = nil +} + +func (self *MenuViewModel) GetPromptLines() []string { + return self.promptLines +} + +func (self *MenuViewModel) SetPromptLines(promptLines []string) { + self.promptLines = promptLines +} + // TODO: move into presentation package func (self *MenuViewModel) GetDisplayStrings(_ int, _ int) [][]string { menuItems := self.FilteredListViewModel.GetItems() @@ -94,14 +113,22 @@ func (self *MenuViewModel) GetDisplayStrings(_ int, _ int) [][]string { } func (self *MenuViewModel) GetNonModelItems() []*NonModelItem { + result := []*NonModelItem{} + result = append(result, lo.Map(self.promptLines, func(line string, _ int) *NonModelItem { + return &NonModelItem{ + Index: 0, + Column: 0, + Content: line, + } + })...) + // Don't display section headers when we are filtering, and the filter mode // is fuzzy. The reason is that filtering changes the order of the items // (they are sorted by best match), so all the sections would be messed up. if self.FilteredListViewModel.IsFiltering() && self.c.UserConfig.Gui.UseFuzzySearch() { - return []*NonModelItem{} + return result } - result := []*NonModelItem{} menuItems := self.FilteredListViewModel.GetItems() var prevSection *types.MenuSection = nil for i, menuItem := range menuItems { diff --git a/pkg/gui/controllers/helpers/confirmation_helper.go b/pkg/gui/controllers/helpers/confirmation_helper.go index 7801081fb..8f7c60b5a 100644 --- a/pkg/gui/controllers/helpers/confirmation_helper.go +++ b/pkg/gui/controllers/helpers/confirmation_helper.go @@ -373,7 +373,9 @@ func (self *ConfirmationHelper) resizeMenu() { itemCount := self.c.Contexts().Menu.UnfilteredLen() offset := 3 panelWidth := self.getPopupPanelWidth() - x0, y0, x1, y1 := self.getPopupPanelDimensionsForContentHeight(panelWidth, itemCount+offset) + contentWidth := panelWidth - 2 // minus 2 for the frame + promptLinesCount := self.layoutMenuPrompt(contentWidth) + x0, y0, x1, y1 := self.getPopupPanelDimensionsForContentHeight(panelWidth, itemCount+offset+promptLinesCount) menuBottom := y1 - offset _, _ = self.c.GocuiGui().SetView(self.c.Views().Menu.Name(), x0, y0, x1, menuBottom, 0) @@ -383,11 +385,39 @@ func (self *ConfirmationHelper) resizeMenu() { if selectedItem != nil { tooltip = self.TooltipForMenuItem(selectedItem) } - contentWidth := panelWidth - 2 // minus 2 for the frame tooltipHeight := getMessageHeight(true, tooltip, contentWidth) + 2 // plus 2 for the frame _, _ = self.c.GocuiGui().SetView(self.c.Views().Tooltip.Name(), x0, tooltipTop, x1, tooltipTop+tooltipHeight-1, 0) } +// Wraps the lines of the menu prompt to the available width and rerenders the +// menu if neeeded. Returns the number of lines the prompt takes up. +func (self *ConfirmationHelper) layoutMenuPrompt(contentWidth int) int { + oldPromptLines := self.c.Contexts().Menu.GetPromptLines() + var promptLines []string + prompt := self.c.Contexts().Menu.GetPrompt() + if len(prompt) > 0 { + promptLines = wrapMessageToWidth(true, prompt, contentWidth) + promptLines = append(promptLines, "") + } + self.c.Contexts().Menu.SetPromptLines(promptLines) + if len(oldPromptLines) != len(promptLines) { + // The number of lines in the prompt has changed; this happens either + // because we're now showing a menu that has a prompt, and the previous + // menu didn't (or vice versa), or because the user is resizing the + // terminal window while a menu with a prompt is open. + + // We need to rerender to give the menu context a chance to update its + // non-model items, and reinitialize the data it uses for converting + // between view index and model index. + _ = self.c.Contexts().Menu.HandleRender() + + // Then we need to refocus to ensure the cursor is in the right place in + // the view. + _ = self.c.Contexts().Menu.HandleFocus(types.OnFocusOpts{}) + } + return len(promptLines) +} + func (self *ConfirmationHelper) resizeConfirmationPanel() { suggestionsViewHeight := 0 if self.c.Views().Suggestions.Visible { diff --git a/pkg/gui/menu_panel.go b/pkg/gui/menu_panel.go index b777536ee..ca03ea69e 100644 --- a/pkg/gui/menu_panel.go +++ b/pkg/gui/menu_panel.go @@ -42,6 +42,7 @@ func (gui *Gui) createMenu(opts types.CreateMenuOptions) error { } gui.State.Contexts.Menu.SetMenuItems(opts.Items, opts.ColumnAlignment) + gui.State.Contexts.Menu.SetPrompt(opts.Prompt) gui.State.Contexts.Menu.SetSelection(0) gui.Views.Menu.Title = opts.Title diff --git a/pkg/gui/types/common.go b/pkg/gui/types/common.go index 77f2f56eb..fc9168406 100644 --- a/pkg/gui/types/common.go +++ b/pkg/gui/types/common.go @@ -159,6 +159,7 @@ const ( type CreateMenuOptions struct { Title string + Prompt string // a message that will be displayed above the menu options Items []*MenuItem HideCancel bool ColumnAlignment []utils.Alignment