From 2e4fbdcf0ac1612ca6ce0c46c0ca36ffc8921a51 Mon Sep 17 00:00:00 2001 From: muffinmad Date: Fri, 13 Jun 2025 21:20:49 +0200 Subject: [PATCH 1/5] Implement ChoiceModal --- pkg/tui/helper.go | 4 + pkg/tui/primitive/choicemodal.go | 135 +++++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+) create mode 100644 pkg/tui/primitive/choicemodal.go diff --git a/pkg/tui/helper.go b/pkg/tui/helper.go index d35a1a7b..2aab732c 100644 --- a/pkg/tui/helper.go +++ b/pkg/tui/helper.go @@ -66,6 +66,10 @@ func getActionModal() *primitive.ActionModal { 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. // // Dumb terminal indicates terminal with limited capability. It may not provide support diff --git a/pkg/tui/primitive/choicemodal.go b/pkg/tui/primitive/choicemodal.go new file mode 100644 index 00000000..ca5b50c0 --- /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 len(line) == 0 { + lines = append(lines, "") + 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) +} From b8cfefcf9c4928eb8bf4aef90ebd2d4290c10242 Mon Sep 17 00:00:00 2001 From: muffinmad Date: Fri, 13 Jun 2025 21:21:13 +0200 Subject: [PATCH 2/5] Use ChoiceModal to select status --- pkg/tui/table.go | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/pkg/tui/table.go b/pkg/tui/table.go index 2d1a2a46..4880c8a4 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(btnIndex 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 { From 60b65a8f81866fc3337a00dfce22e2b48ffb6724 Mon Sep 17 00:00:00 2001 From: muffinmad Date: Fri, 13 Jun 2025 21:27:48 +0200 Subject: [PATCH 3/5] Remove unused code --- pkg/tui/helper.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/pkg/tui/helper.go b/pkg/tui/helper.go index 2aab732c..bf5f54a8 100644 --- a/pkg/tui/helper.go +++ b/pkg/tui/helper.go @@ -59,13 +59,6 @@ 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() } From fa4c0960aa20d78440ad5785c324d7fab26751bb Mon Sep 17 00:00:00 2001 From: muffinmad Date: Fri, 13 Jun 2025 21:33:52 +0200 Subject: [PATCH 4/5] Rename unused parameter --- pkg/tui/table.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/tui/table.go b/pkg/tui/table.go index 4880c8a4..8087062f 100644 --- a/pkg/tui/table.go +++ b/pkg/tui/table.go @@ -356,7 +356,7 @@ func (t *Table) initTable() { fmt.Sprintf("Select desired state to transition %s to:", key), ) - t.choice.SetDoneFunc(func(btnIndex int, btnLabel string) { + t.choice.SetDoneFunc(func(_ int, btnLabel string) { t.choice.GetFooter().SetText("Processing. Please wait...").SetTextColor(tcell.ColorGray) t.screen.ForceDraw() From 7364b935306bc3d7492b8c0323254a75683c2731 Mon Sep 17 00:00:00 2001 From: muffinmad Date: Fri, 13 Jun 2025 21:34:11 +0200 Subject: [PATCH 5/5] Improved empty string test --- pkg/tui/primitive/choicemodal.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/tui/primitive/choicemodal.go b/pkg/tui/primitive/choicemodal.go index ca5b50c0..65bb2972 100644 --- a/pkg/tui/primitive/choicemodal.go +++ b/pkg/tui/primitive/choicemodal.go @@ -111,8 +111,8 @@ func (m *ChoiceModal) Draw(screen tcell.Screen) { m.frame.Clear() var lines []string for _, line := range strings.Split(m.text, "\n") { - if len(line) == 0 { - lines = append(lines, "") + if line == "" { + lines = append(lines, line) continue } lines = append(lines, tview.WordWrap(line, width)...)