Skip to content

Commit f96029e

Browse files
author
Ismar Iljazovic
committed
feat: use list to select transition status (ankitpokhrel#869)
Cherry-picked from upstream PR ankitpokhrel#869 by muffinmad. Adds a ChoiceModal for selecting issue transition status instead of buttons.
1 parent d836c1b commit f96029e

File tree

3 files changed

+170
-18
lines changed

3 files changed

+170
-18
lines changed

pkg/tui/helper.go

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,8 @@ func getInfoModal() *tview.Modal {
5959
return modal
6060
}
6161

62-
func getActionModal() *primitive.ActionModal {
63-
return primitive.NewActionModal().
64-
SetBackgroundColor(tcell.ColorSpecial).
65-
SetButtonBackgroundColor(tcell.ColorDarkCyan).
66-
SetTextColor(tcell.ColorDefault)
62+
func getChoiceModal() *primitive.ChoiceModal {
63+
return primitive.NewChoiceModal()
6764
}
6865

6966
// IsDumbTerminal checks TERM/WT_SESSION environment variable and returns true if they indicate a dumb terminal.

pkg/tui/primitive/choicemodal.go

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
package primitive
2+
3+
import (
4+
"strings"
5+
6+
"github.com/gdamore/tcell/v2"
7+
"github.com/rivo/tview"
8+
)
9+
10+
// ChoiceModal is a modal dialog that presents a list of choices to the user.
11+
type ChoiceModal struct {
12+
*tview.Box
13+
frame *tview.Frame
14+
text string
15+
list *tview.List
16+
footer *tview.TextView
17+
done func(index int, label string)
18+
}
19+
20+
// Choice modal layout constants.
21+
const (
22+
choiceFooterHeight = 2
23+
)
24+
25+
// NewChoiceModal creates a new choice modal.
26+
func NewChoiceModal() *ChoiceModal {
27+
m := &ChoiceModal{Box: tview.NewBox()}
28+
29+
m.list = tview.NewList().
30+
ShowSecondaryText(false).
31+
SetMainTextColor(tcell.ColorDefault)
32+
33+
m.footer = tview.NewTextView()
34+
m.footer.SetTitleAlign(tview.AlignCenter)
35+
m.footer.SetTextAlign(tview.AlignCenter)
36+
m.footer.SetTextStyle(tcell.StyleDefault.Italic(true))
37+
m.footer.SetBorderPadding(1, 0, 0, 0)
38+
39+
flex := tview.NewFlex().SetDirection(tview.FlexRow).
40+
AddItem(m.list, 0, 1, true).
41+
AddItem(m.footer, choiceFooterHeight, 0, false)
42+
43+
m.frame = tview.NewFrame(flex).SetBorders(0, 0, 1, 0, 0, 0)
44+
m.frame.SetBorder(true).SetBorderPadding(1, 1, 1, 1)
45+
46+
return m
47+
}
48+
49+
// SetText sets the text displayed in the modal.
50+
func (m *ChoiceModal) SetText(text string) {
51+
m.text = text
52+
}
53+
54+
// SetDoneFunc sets the callback function when a choice is selected.
55+
func (m *ChoiceModal) SetDoneFunc(doneFunc func(index int, label string)) *ChoiceModal {
56+
m.done = doneFunc
57+
return m
58+
}
59+
60+
// SetChoices sets the list of choices to display.
61+
func (m *ChoiceModal) SetChoices(choices []string) *ChoiceModal {
62+
m.list.Clear()
63+
for _, choice := range choices {
64+
m.list.AddItem(choice, "", 0, nil)
65+
}
66+
return m
67+
}
68+
69+
// SetSelected sets the currently selected choice index.
70+
func (m *ChoiceModal) SetSelected(index int) *ChoiceModal {
71+
m.list.SetCurrentItem(index)
72+
return m
73+
}
74+
75+
// GetFooter returns the footer text view.
76+
func (m *ChoiceModal) GetFooter() *tview.TextView {
77+
return m.footer
78+
}
79+
80+
// Focus is called when this primitive receives focus.
81+
func (m *ChoiceModal) Focus(delegate func(p tview.Primitive)) {
82+
delegate(m.list)
83+
}
84+
85+
// HasFocus returns whether or not this primitive has focus.
86+
func (m *ChoiceModal) HasFocus() bool {
87+
return m.list.HasFocus()
88+
}
89+
90+
// InputHandler returns the handler for this primitive.
91+
func (m *ChoiceModal) InputHandler() func(event *tcell.EventKey, setFocus func(p tview.Primitive)) {
92+
return m.WrapInputHandler(func(event *tcell.EventKey, setFocus func(p tview.Primitive)) {
93+
switch event.Key() {
94+
case tcell.KeyEnter:
95+
if m.done != nil {
96+
index := m.list.GetCurrentItem()
97+
label, _ := m.list.GetItemText(index)
98+
m.done(index, label)
99+
}
100+
default:
101+
if handler := m.frame.InputHandler(); handler != nil {
102+
handler(event, setFocus)
103+
}
104+
}
105+
})
106+
}
107+
108+
// MouseHandler returns the mouse handler for this primitive.
109+
func (m *ChoiceModal) MouseHandler() func(action tview.MouseAction, event *tcell.EventMouse, setFocus func(p tview.Primitive)) (bool, tview.Primitive) {
110+
return m.WrapMouseHandler(func(action tview.MouseAction, event *tcell.EventMouse, setFocus func(p tview.Primitive)) (bool, tview.Primitive) {
111+
if handler := m.frame.MouseHandler(); handler != nil {
112+
return handler(action, event, setFocus)
113+
}
114+
return false, nil
115+
})
116+
}
117+
118+
// Choice modal Draw constants.
119+
const (
120+
choiceVerticalMargin = 3
121+
choiceFrameExtraHeight = 7
122+
choiceModalWidth = 70
123+
marginMultiplier = 2
124+
)
125+
126+
// Draw draws this primitive onto the screen.
127+
func (m *ChoiceModal) Draw(screen tcell.Screen) {
128+
screenWidth, screenHeight := screen.Size()
129+
width := choiceModalWidth
130+
131+
m.frame.Clear()
132+
var lines []string
133+
for _, line := range strings.Split(m.text, "\n") {
134+
if line == "" {
135+
lines = append(lines, line)
136+
continue
137+
}
138+
lines = append(lines, tview.WordWrap(line, width)...)
139+
}
140+
141+
for _, line := range lines {
142+
m.frame.AddText(line, true, tview.AlignCenter, tcell.ColorDefault)
143+
}
144+
145+
height := len(lines) + m.list.GetItemCount() + choiceFrameExtraHeight
146+
maxHeight := screenHeight - choiceVerticalMargin*marginMultiplier
147+
if height > maxHeight {
148+
height = maxHeight
149+
}
150+
151+
x := (screenWidth - width) / centerDivisor
152+
y := (screenHeight - height) / centerDivisor
153+
m.frame.SetRect(x, y, width, height)
154+
m.frame.Draw(screen)
155+
}

pkg/tui/table.go

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ type Table struct {
8888
footer *tview.TextView
8989
secondary *tview.Modal
9090
help *primitive.InfoModal
91-
action *primitive.ActionModal
91+
choice *primitive.ChoiceModal
9292
style TableStyle
9393
data TableData
9494
colPad uint
@@ -117,7 +117,7 @@ func NewTable(opts ...TableOption) *Table {
117117
footer: tview.NewTextView(),
118118
help: primitive.NewInfoModal(),
119119
secondary: getInfoModal(),
120-
action: getActionModal(),
120+
choice: getChoiceModal(),
121121
colPad: defaultColPad,
122122
maxColWidth: defaultColWidth,
123123
}
@@ -141,9 +141,9 @@ func NewTable(opts ...TableOption) *Table {
141141
AddItem(tview.NewTextView(), rowSpacer, 0, 1, 1, 0, 0, false). // Dummy view to fake row padding.
142142
AddItem(tbl.footer, rowFooter, 0, 1, 1, 0, 0, false)
143143

144-
tbl.action.SetInputCapture(func(ev *tcell.EventKey) *tcell.EventKey {
144+
tbl.choice.SetInputCapture(func(ev *tcell.EventKey) *tcell.EventKey {
145145
if ev.Key() == tcell.KeyEsc || (ev.Key() == tcell.KeyRune && ev.Rune() == 'q') {
146-
tbl.painter.HidePage("action")
146+
tbl.painter.HidePage("choice")
147147
}
148148
return ev
149149
})
@@ -152,7 +152,7 @@ func NewTable(opts ...TableOption) *Table {
152152
AddPage("primary", grid, true, true).
153153
AddPage("secondary", tbl.secondary, true, false).
154154
AddPage("help", tbl.help, true, false).
155-
AddPage("action", tbl.action, true, false)
155+
AddPage("choice", tbl.choice, true, false)
156156

157157
return &tbl
158158
}
@@ -333,15 +333,15 @@ func (t *Table) initTable() {
333333
}
334334

335335
refreshContextInFooter := func() {
336-
t.action.GetFooter().SetText("Use TAB or ← → to navigate, ENTER to select, ESC or q to cancel.").SetTextColor(tcell.ColorGray)
336+
t.choice.GetFooter().SetText("Use TAB or ↑ ↓ to navigate, ENTER to select, ESC or q to cancel.").SetTextColor(tcell.ColorGray)
337337
}
338338

339339
go func() {
340340
func() {
341341
t.painter.ShowPage("secondary").SendToFront("secondary")
342342
defer func() {
343343
t.painter.HidePage("secondary")
344-
t.painter.ShowPage("action")
344+
t.painter.ShowPage("choice")
345345
}()
346346
refreshContextInFooter()
347347

@@ -357,23 +357,23 @@ func (t *Table) initTable() {
357357
return 0
358358
}
359359

360-
t.action.ClearButtons().AddButtons(actions).SetFocus(currentStatusIdx())
361-
t.action.SetText(
360+
t.choice.SetChoices(actions).SetSelected(currentStatusIdx())
361+
t.choice.SetText(
362362
fmt.Sprintf("Select desired state to transition %s to:", key),
363363
)
364364

365-
t.action.SetDoneFunc(func(_ int, btnLabel string) {
366-
t.action.GetFooter().SetText("Processing. Please wait...").SetTextColor(tcell.ColorGray)
365+
t.choice.SetDoneFunc(func(_ int, btnLabel string) {
366+
t.choice.GetFooter().SetText("Processing. Please wait...").SetTextColor(tcell.ColorGray)
367367
t.screen.ForceDraw()
368368

369369
err := handler(btnLabel)
370370
if err != nil {
371-
t.action.GetFooter().SetText(
371+
t.choice.GetFooter().SetText(
372372
fmt.Sprintf("Error: %s", err.Error()),
373373
).SetTextColor(tcell.ColorRed)
374374
return
375375
}
376-
t.painter.HidePage("action")
376+
t.painter.HidePage("choice")
377377
refreshContextInFooter()
378378

379379
if refreshFunc != nil {

0 commit comments

Comments
 (0)