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  }