Skip to content

Commit e410a70

Browse files
committed
feat: apply flow-integration improvements
1 parent 001adac commit e410a70

15 files changed

Lines changed: 754 additions & 146 deletions

File tree

container.go

Lines changed: 99 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@ type InputCapturer interface {
3333
CapturingInput() bool
3434
}
3535

36+
// BackHandler is an optional interface views can implement to intercept back
37+
// navigation events before the Container forcefully pops the view stack.
38+
type BackHandler interface {
39+
HandleBack() bool
40+
}
41+
3642
type Container struct {
3743
ctx context.Context
3844
cancel context.CancelFunc
@@ -41,10 +47,11 @@ type Container struct {
4147
render *types.RenderState
4248
sendFunc func(msg tea.Msg) // Temporary hack for testing
4349

44-
previousView, currentView, nextView View
45-
help *overlay.HelpPopup
46-
toasts *overlay.ToastManager
47-
finalizing *chan struct{}
50+
currentView, nextView View
51+
viewStack []View
52+
help *overlay.HelpPopup
53+
toasts *overlay.ToastManager
54+
finalizing *chan struct{}
4855

4956
viewMu sync.RWMutex
5057
stateMu sync.RWMutex
@@ -156,7 +163,6 @@ func (c *Container) Init() tea.Cmd {
156163
return tea.Batch(cmds...)
157164
}
158165

159-
//nolint:gocognit,funlen
160166
func (c *Container) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
161167
fwdMsg := msg
162168
cmds := make([]tea.Cmd, 0)
@@ -183,8 +189,8 @@ func (c *Container) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
183189
switch {
184190
case c.NextView() != nil:
185191
err = c.SetView(c.NextView())
186-
case c.PreviousView() != nil:
187-
err = c.SetView(c.PreviousView())
192+
case len(c.viewStack) > 0:
193+
err = c.PopView()
188194
case c.CurrentView().Type() == views.FormViewType:
189195
return c, tea.Quit
190196
default:
@@ -194,54 +200,18 @@ func (c *Container) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
194200
c.HandleError(err)
195201
}
196202
case tea.KeyPressMsg:
197-
if c.help.Visible() {
198-
fwdMsg = nil
199-
switch msg.String() {
200-
case "esc", "h", "?":
201-
c.help.Toggle()
202-
}
203-
break
204-
}
205-
if c.CurrentView().Type() == views.FormViewType {
206-
fwdMsg = nil
207-
_, cmd := c.CurrentView().Update(msg)
208-
cmds = append(cmds, cmd)
209-
break
210-
}
211-
// When the view is capturing input (e.g. filter field), only
212-
// handle hard exit keys; forward everything else to the view.
213-
if ic, ok := c.CurrentView().(InputCapturer); ok && ic.CapturingInput() {
214-
if msg.String() == "ctrl+c" {
215-
c.CurrentView().Update(tea.Quit())
216-
return c, tea.Quit
217-
}
218-
break
219-
}
220-
switch msg.String() {
221-
case "ctrl+c", "q":
222-
c.CurrentView().Update(tea.Quit())
223-
return c, tea.Quit
224-
case "esc", "backspace":
225-
if c.PreviousView() == nil || c.CurrentView() == c.PreviousView() {
226-
c.CurrentView().Update(tea.Quit())
227-
return c, tea.Quit
228-
} else {
229-
if err := c.SetView(c.PreviousView()); err != nil {
230-
c.HandleError(err)
231-
}
232-
return c, nil
233-
}
234-
case "h", "?":
235-
fwdMsg = nil
236-
c.help.SetViewKeys(c.CurrentView().HelpBindings())
237-
c.help.Toggle()
203+
model, cmd, handled := c.handleKeyPress(msg)
204+
if handled {
205+
return model, cmd
238206
}
207+
fwdMsg, cmds = c.updateKeyFwdMsg(msg, fwdMsg, cmds)
239208
case types.ToastDismissMsg:
240209
c.toasts.Dismiss(msg.ID)
241210
case types.TickMsg:
242211
if c.Ready() && c.CurrentView().Type() == views.LoadingViewType && c.nextView != nil {
243212
c.currentView = c.nextView
244213
c.nextView = nil
214+
cmds = append(cmds, c.currentView.Init())
245215
}
246216
cmds = append(cmds, types.Tick)
247217
case tea.Cmd:
@@ -254,6 +224,62 @@ func (c *Container) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
254224
return c, tea.Batch(cmds...)
255225
}
256226

227+
// handleKeyPress handles key events that produce an early return from Update.
228+
// It returns (model, cmd, true) when the caller should return immediately.
229+
func (c *Container) handleKeyPress(msg tea.KeyPressMsg) (tea.Model, tea.Cmd, bool) {
230+
switch msg.String() {
231+
case "ctrl+c", "q":
232+
c.CurrentView().Update(tea.Quit())
233+
return c, tea.Quit, true
234+
case "esc", types.KeyBackspace:
235+
if bh, ok := c.CurrentView().(BackHandler); ok && bh.HandleBack() {
236+
return c, nil, false
237+
}
238+
c.viewMu.RLock()
239+
stackLen := len(c.viewStack)
240+
c.viewMu.RUnlock()
241+
if stackLen == 0 {
242+
c.CurrentView().Update(tea.Quit())
243+
return c, tea.Quit, true
244+
}
245+
if err := c.PopView(); err != nil {
246+
c.HandleError(err)
247+
}
248+
return c, nil, true
249+
}
250+
return c, nil, false
251+
}
252+
253+
// updateKeyFwdMsg decides how a key press should be forwarded to the current view.
254+
func (c *Container) updateKeyFwdMsg(
255+
msg tea.KeyPressMsg, fwdMsg tea.Msg, cmds []tea.Cmd,
256+
) (tea.Msg, []tea.Cmd) {
257+
if c.help.Visible() {
258+
switch msg.String() {
259+
case "esc", "h", "?":
260+
c.help.Toggle()
261+
}
262+
return nil, cmds
263+
}
264+
if c.CurrentView().Type() == views.FormViewType {
265+
_, cmd := c.CurrentView().Update(msg)
266+
return nil, append(cmds, cmd)
267+
}
268+
if ic, ok := c.CurrentView().(InputCapturer); ok && ic.CapturingInput() {
269+
if msg.String() == "ctrl+c" {
270+
c.CurrentView().Update(tea.Quit())
271+
}
272+
return fwdMsg, cmds
273+
}
274+
switch msg.String() {
275+
case "h", "?":
276+
c.help.SetViewKeys(c.CurrentView().HelpBindings())
277+
c.help.Toggle()
278+
return nil, cmds
279+
}
280+
return fwdMsg, cmds
281+
}
282+
257283
func (c *Container) View() tea.View {
258284
if !c.Ready() && c.CurrentView().Type() != views.LoadingViewType {
259285
return tea.NewView("")
@@ -361,7 +387,7 @@ func (c *Container) SetView(v View) error {
361387
c.SetNextView(v)
362388
case switching:
363389
c.viewMu.Lock()
364-
c.previousView = c.currentView
390+
c.viewStack = append(c.viewStack, c.currentView)
365391
c.viewMu.Unlock()
366392
fallthrough
367393
default:
@@ -414,7 +440,30 @@ func (c *Container) CurrentView() View {
414440
func (c *Container) PreviousView() View {
415441
c.viewMu.RLock()
416442
defer c.viewMu.RUnlock()
417-
return c.previousView
443+
if len(c.viewStack) == 0 {
444+
return nil
445+
}
446+
return c.viewStack[len(c.viewStack)-1]
447+
}
448+
449+
func (c *Container) PopView() error {
450+
c.viewMu.Lock()
451+
if len(c.viewStack) == 0 {
452+
c.viewMu.Unlock()
453+
return errors.New("no previous view in stack")
454+
}
455+
prev := c.viewStack[len(c.viewStack)-1]
456+
c.viewStack = c.viewStack[:len(c.viewStack)-1]
457+
c.currentView = prev
458+
if c.currentView == c.nextView {
459+
c.nextView = nil
460+
}
461+
c.viewMu.Unlock()
462+
cmd := c.CurrentView().Init()
463+
if cmd != nil {
464+
c.Send(cmd, 0)
465+
}
466+
return nil
418467
}
419468

420469
func (c *Container) NextView() View {

container_test.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -615,28 +615,28 @@ func TestLibraryNavigateBackward(t *testing.T) {
615615
}
616616
}
617617

618-
func TestLibraryCapturingInput(t *testing.T) {
618+
func TestLibraryBackHandler(t *testing.T) {
619619
lib := testLibrary()
620-
ic, ok := any(lib).(tuikit.InputCapturer)
620+
bh, ok := any(lib).(tuikit.BackHandler)
621621
if !ok {
622-
t.Fatal("Library should implement InputCapturer")
622+
t.Fatal("Library should implement BackHandler")
623623
}
624624

625-
// On page 0, not capturing
626-
if ic.CapturingInput() {
627-
t.Error("should not be capturing input on page 0")
625+
// On page 0, not handling back
626+
if bh.HandleBack() {
627+
t.Error("should not be handling back on page 0")
628628
}
629629

630630
// Navigate to page 1
631631
lib.Update(tea.KeyPressMsg{Code: tea.KeyEnter})
632-
if !ic.CapturingInput() {
633-
t.Error("should be capturing input on page > 0")
632+
if !bh.HandleBack() {
633+
t.Error("should be handling back on page > 0")
634634
}
635635

636636
// Navigate back to page 0
637637
lib.Update(tea.KeyPressMsg{Code: tea.KeyEscape})
638-
if ic.CapturingInput() {
639-
t.Error("should not be capturing input after returning to page 0")
638+
if bh.HandleBack() {
639+
t.Error("should not be handling back after returning to page 0")
640640
}
641641
}
642642

go.mod

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ require (
1010
charm.land/lipgloss/v2 v2.0.2
1111
charm.land/log/v2 v2.0.0
1212
github.com/charmbracelet/colorprofile v0.4.3
13+
github.com/charmbracelet/x/ansi v0.11.6
1314
github.com/charmbracelet/x/exp/teatest/v2 v2.0.0-20260330094520-2dce04b6f8a4
1415
github.com/muesli/reflow v0.3.0
15-
github.com/muesli/termenv v0.16.0
1616
go.uber.org/mock v0.6.0
1717
golang.org/x/term v0.41.0
1818
gopkg.in/yaml.v3 v3.0.1
@@ -21,12 +21,10 @@ require (
2121
require (
2222
github.com/alecthomas/chroma/v2 v2.14.0 // indirect
2323
github.com/atotto/clipboard v0.1.4 // indirect
24-
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
2524
github.com/aymanbagabas/go-udiff v0.4.1 // indirect
2625
github.com/aymerick/douceur v0.2.0 // indirect
2726
github.com/catppuccin/go v0.3.0 // indirect
2827
github.com/charmbracelet/ultraviolet v0.0.0-20260205113103-524a6607adb8 // indirect
29-
github.com/charmbracelet/x/ansi v0.11.6 // indirect
3028
github.com/charmbracelet/x/exp/golden v0.0.0-20251109135125-8916d276318f // indirect
3129
github.com/charmbracelet/x/exp/ordered v0.1.0 // indirect
3230
github.com/charmbracelet/x/exp/slice v0.0.0-20250327172914-2fdc97757edf // indirect
@@ -41,7 +39,6 @@ require (
4139
github.com/go-logfmt/logfmt v0.6.0 // indirect
4240
github.com/gorilla/css v1.0.1 // indirect
4341
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
44-
github.com/mattn/go-isatty v0.0.20 // indirect
4542
github.com/mattn/go-runewidth v0.0.21 // indirect
4643
github.com/microcosm-cc/bluemonday v1.0.27 // indirect
4744
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect

go.sum

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc
2020
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
2121
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
2222
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
23-
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
24-
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
2523
github.com/aymanbagabas/go-udiff v0.4.1 h1:OEIrQ8maEeDBXQDoGCbbTTXYJMYRCRO1fnodZ12Gv5o=
2624
github.com/aymanbagabas/go-udiff v0.4.1/go.mod h1:0L9PGwj20lrtmEMeyw4WKJ/TMyDtvAoK9bf2u/mNo3w=
2725
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
@@ -80,8 +78,6 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
8078
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
8179
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
8280
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
83-
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
84-
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
8581
github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk=
8682
github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w=
8783
github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
@@ -93,8 +89,6 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU
9389
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
9490
github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s=
9591
github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8=
96-
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
97-
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
9892
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
9993
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
10094
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
@@ -122,7 +116,6 @@ golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
122116
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
123117
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
124118
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
125-
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
126119
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
127120
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
128121
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=

0 commit comments

Comments
 (0)