@@ -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+
3642type 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
160166func (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+
257283func (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 {
414440func (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
420469func (c * Container ) NextView () View {
0 commit comments