github.com/wolfi-dev/wolfictl@v0.16.11/pkg/cli/components/ctrlcwrapper/ctrlcwrapper.go (about) 1 package ctrlcwrapper 2 3 import ( 4 "time" 5 6 tea "github.com/charmbracelet/bubbletea" 7 ) 8 9 type Model[T tea.Model] struct { 10 userWantsToExit bool 11 12 inner T 13 } 14 15 // Any is a type that can be used to check if a model is a Model 16 // without knowing the inner type. 17 type Any interface { 18 UserWantsToExit() bool 19 } 20 21 type AboutToExitMsg struct{} 22 23 // InnerIsReady is a tea.Cmd that the inner model can send to the wrapper model 24 // to indicate that it's ready to exit. This is helpful to the overall program 25 // because it allows the wrapper to exit earlier than it would if it waited for 26 // its own internal tick expiration event. 27 func InnerIsReady() tea.Msg { 28 return innerIsReadyMsg{} 29 } 30 31 type innerIsReadyMsg struct{} 32 33 type tickExpiredMsg struct{} 34 35 // New returns a new model that wraps the given model. The new model will exit 36 // when the user presses Ctrl+C. 37 func New[T tea.Model](inner T) tea.Model { 38 return Model[T]{ 39 inner: inner, 40 } 41 } 42 43 // Unwrap returns the inner model with its original type. 44 func (m Model[T]) Unwrap() T { 45 return m.inner 46 } 47 48 // UserWantsToExit returns true if the user pressed Ctrl+C. This can be used 49 // when the bubbletea program exits to determine if the user wants to exit the 50 // program. 51 func (m Model[T]) UserWantsToExit() bool { 52 return m.userWantsToExit 53 } 54 55 func (m Model[T]) Init() tea.Cmd { 56 return m.inner.Init() 57 } 58 59 func (m Model[T]) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 60 switch msg := msg.(type) { 61 case tea.KeyMsg: 62 if msg.String() == "ctrl+c" { 63 m.userWantsToExit = true 64 65 // Tell the inner model that we're about to exit. Cmds aren't supported 66 // currently. 67 updated, _ := m.inner.Update(AboutToExitMsg{}) 68 var inner T 69 var ok bool 70 if inner, ok = updated.(T); !ok { 71 // Nothing we can do here, but this shouldn't ever happen. 72 return m, nil 73 } 74 75 m.inner = inner 76 77 // We'll give the inner model a second to clean up before we exit. This is like 78 // a SIGINT. 79 delayedExitCmd := tea.Tick(1*time.Second, func(time.Time) tea.Msg { 80 return tickExpiredMsg{} 81 }) 82 83 return m, delayedExitCmd 84 } 85 86 case innerIsReadyMsg, tickExpiredMsg: 87 // The inner has finished its cleanup and is ready to exit. 88 // Or, the "SIGINT" delay has expired, so we're going to exit anyway! 89 return m, tea.Quit 90 } 91 92 // Normal proxying of messages to the inner model. 93 updated, cmd := m.inner.Update(msg) 94 var inner T 95 var ok bool 96 if inner, ok = updated.(T); !ok { 97 // Nothing we can do here, but this shouldn't ever happen. 98 return m, nil 99 } 100 101 m.inner = inner 102 103 return m, cmd 104 } 105 106 func (m Model[T]) View() string { 107 return m.inner.View() 108 }