diff --git a/pkg/tui/helper.go b/pkg/tui/helper.go index d35a1a7b..bf5f54a8 100644 --- a/pkg/tui/helper.go +++ b/pkg/tui/helper.go @@ -59,11 +59,8 @@ func getInfoModal() *tview.Modal { return modal } -func getActionModal() *primitive.ActionModal { - return primitive.NewActionModal(). - SetBackgroundColor(tcell.ColorSpecial). - SetButtonBackgroundColor(tcell.ColorDarkCyan). - SetTextColor(tcell.ColorDefault) +func getChoiceModal() *primitive.ChoiceModal { + return primitive.NewChoiceModal() } // IsDumbTerminal checks TERM/WT_SESSION environment variable and returns true if they indicate a dumb terminal. diff --git a/pkg/tui/primitive/choicemodal.go b/pkg/tui/primitive/choicemodal.go new file mode 100644 index 00000000..65bb2972 --- /dev/null +++ b/pkg/tui/primitive/choicemodal.go @@ -0,0 +1,135 @@ +package primitive + +import ( + "strings" + + "github.com/gdamore/tcell/v2" + "github.com/rivo/tview" +) + +type ChoiceModal struct { + *tview.Box + frame *tview.Frame + text string + list *tview.List + footer *tview.TextView + done func(index int, label string) +} + +func NewChoiceModal() *ChoiceModal { + m := &ChoiceModal{Box: tview.NewBox()} + + m.list = tview.NewList(). + ShowSecondaryText(false). + SetMainTextColor(tcell.ColorDefault) + + m.footer = tview.NewTextView() + m.footer.SetTitleAlign(tview.AlignCenter) + m.footer.SetTextAlign(tview.AlignCenter) + m.footer.SetTextStyle(tcell.StyleDefault.Italic(true)) + m.footer.SetBorderPadding(1, 0, 0, 0) + + flex := tview.NewFlex().SetDirection(tview.FlexRow). + AddItem(m.list, 0, 1, true). + AddItem(m.footer, 2, 0, false) + + m.frame = tview.NewFrame(flex).SetBorders(0, 0, 1, 0, 0, 0) + m.frame.SetBorder(true).SetBorderPadding(1, 1, 1, 1) + + return m +} + +func (m *ChoiceModal) SetText(text string) { + m.text = text +} + +func (m *ChoiceModal) SetDoneFunc(doneFunc func(index int, label string)) *ChoiceModal { + m.done = doneFunc + return m +} + +func (m *ChoiceModal) SetChoices(choices []string) *ChoiceModal { + m.list.Clear() + for _, choice := range choices { + m.list.AddItem(choice, "", 0, nil) + } + return m +} + +func (m *ChoiceModal) SetSelected(index int) *ChoiceModal { + m.list.SetCurrentItem(index) + return m +} + +func (m *ChoiceModal) GetFooter() *tview.TextView { + return m.footer +} + +func (m *ChoiceModal) Focus(delegate func(p tview.Primitive)) { + delegate(m.list) +} + +func (m *ChoiceModal) HasFocus() bool { + return m.list.HasFocus() +} + +func (m *ChoiceModal) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) { + return m.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p tview.Primitive)) { + switch event.Key() { + case tcell.KeyEnter: + if m.done != nil { + index := m.list.GetCurrentItem() + label, _ := m.list.GetItemText(index) + m.done(index, label) + } + default: + if handler := m.frame.InputHandler(); handler != nil { + handler(event, setFocus) + } + } + }) +} + +func (m *ChoiceModal) MouseHandler() func(action tview.MouseAction, event *tcell.EventMouse, setFocus func(p tview.Primitive)) (bool, tview.Primitive) { + return m.WrapMouseHandler(func(action tview.MouseAction, event *tcell.EventMouse, setFocus func(p tview.Primitive)) (bool, tview.Primitive) { + if handler := m.frame.MouseHandler(); handler != nil { + return handler(action, event, setFocus) + } + return false, nil + }) +} + +const ( + verticalMargin = 3 + frameExtraHeight = 7 +) + +func (m *ChoiceModal) Draw(screen tcell.Screen) { + screenWidth, screenHeight := screen.Size() + width := 70 + + m.frame.Clear() + var lines []string + for _, line := range strings.Split(m.text, "\n") { + if line == "" { + lines = append(lines, line) + continue + } + lines = append(lines, tview.WordWrap(line, width)...) + } + + for _, line := range lines { + m.frame.AddText(line, true, tview.AlignCenter, tcell.ColorDefault) + } + + height := len(lines) + m.list.GetItemCount() + frameExtraHeight + maxHeight := screenHeight - verticalMargin*2 + if height > maxHeight { + height = maxHeight + } + + x := (screenWidth - width) / 2 + y := (screenHeight - height) / 2 + m.frame.SetRect(x, y, width, height) + m.frame.Draw(screen) +} diff --git a/pkg/tui/table.go b/pkg/tui/table.go index 2d1a2a46..8087062f 100644 --- a/pkg/tui/table.go +++ b/pkg/tui/table.go @@ -88,7 +88,7 @@ type Table struct { footer *tview.TextView secondary *tview.Modal help *primitive.InfoModal - action *primitive.ActionModal + choice *primitive.ChoiceModal style TableStyle data TableData colPad uint @@ -117,7 +117,7 @@ func NewTable(opts ...TableOption) *Table { footer: tview.NewTextView(), help: primitive.NewInfoModal(), secondary: getInfoModal(), - action: getActionModal(), + choice: getChoiceModal(), colPad: defaultColPad, maxColWidth: defaultColWidth, } @@ -135,9 +135,9 @@ func NewTable(opts ...TableOption) *Table { AddItem(tview.NewTextView(), 1, 0, 1, 1, 0, 0, false). // Dummy view to fake row padding. AddItem(tbl.footer, 2, 0, 1, 1, 0, 0, false) - tbl.action.SetInputCapture(func(ev *tcell.EventKey) *tcell.EventKey { + tbl.choice.SetInputCapture(func(ev *tcell.EventKey) *tcell.EventKey { if ev.Key() == tcell.KeyEsc || (ev.Key() == tcell.KeyRune && ev.Rune() == 'q') { - tbl.painter.HidePage("action") + tbl.painter.HidePage("choice") } return ev }) @@ -146,7 +146,7 @@ func NewTable(opts ...TableOption) *Table { AddPage("primary", grid, true, true). AddPage("secondary", tbl.secondary, true, false). AddPage("help", tbl.help, true, false). - AddPage("action", tbl.action, true, false) + AddPage("choice", tbl.choice, true, false) return &tbl } @@ -327,7 +327,7 @@ func (t *Table) initTable() { } refreshContextInFooter := func() { - t.action.GetFooter().SetText("Use TAB or ← → to navigate, ENTER to select, ESC or q to cancel.").SetTextColor(tcell.ColorGray) + t.choice.GetFooter().SetText("Use TAB or ↑ ↓ to navigate, ENTER to select, ESC or q to cancel.").SetTextColor(tcell.ColorGray) } go func() { @@ -335,7 +335,7 @@ func (t *Table) initTable() { t.painter.ShowPage("secondary").SendToFront("secondary") defer func() { t.painter.HidePage("secondary") - t.painter.ShowPage("action") + t.painter.ShowPage("choice") }() refreshContextInFooter() @@ -351,23 +351,23 @@ func (t *Table) initTable() { return 0 } - t.action.ClearButtons().AddButtons(actions).SetFocus(currentStatusIdx()) - t.action.SetText( + t.choice.SetChoices(actions).SetSelected(currentStatusIdx()) + t.choice.SetText( fmt.Sprintf("Select desired state to transition %s to:", key), ) - t.action.SetDoneFunc(func(btnIndex int, btnLabel string) { - t.action.GetFooter().SetText("Processing. Please wait...").SetTextColor(tcell.ColorGray) + t.choice.SetDoneFunc(func(_ int, btnLabel string) { + t.choice.GetFooter().SetText("Processing. Please wait...").SetTextColor(tcell.ColorGray) t.screen.ForceDraw() err := handler(btnLabel) if err != nil { - t.action.GetFooter().SetText( + t.choice.GetFooter().SetText( fmt.Sprintf("Error: %s", err.Error()), ).SetTextColor(tcell.ColorRed) return } - t.painter.HidePage("action") + t.painter.HidePage("choice") refreshContextInFooter() if refreshFunc != nil {