Skip to content

Commit

Permalink
feat: add support for Cmd wrapping (for routing)
Browse files Browse the repository at this point in the history
This is one possible solution for allowing components to wrap any
`tea.Cmd` that comes from a sub-component's `Update()`.  See the comment
in the code for other possible options.
  • Loading branch information
JaredReisinger committed Mar 2, 2024
1 parent a256e76 commit 4420ef4
Show file tree
Hide file tree
Showing 2 changed files with 131 additions and 0 deletions.
60 changes: 60 additions & 0 deletions commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,66 @@ func Sequence(cmds ...Cmd) Cmd {
// sequenceMsg is used internally to run the given commands in order.
type sequenceMsg []Cmd

// Wrap allows a parent component to accurately wrap the results of any Cmd,
// regardless of whether it's a direct Cmd or a collective one like Batch or
// Sequence. Note that the "wrapping" arg could also be an interface{}, or a
// generic type, or even a full `func(Cmd) Msg` (for the most flexibility), but
// using an int seems to satisfy the 80:20 rule of the simplest solution for the
// most-common case: enabling routing for sub-components.
func Wrap(cmd Cmd, id int) Cmd {
if cmd == nil {
return cmd
}

// Both Batch and Sequence are interesting in that they actually create a
// command whose entire purpose is to "return a slice of commands as a new
// message". Because of this, we have let the command execute, and *then*
// detect that it's a Batch or Sequence, then unroll and re-roll with
// individually-wrapped commands.
return func() Msg {
msg := cmd()

var cmds []Cmd

switch msg := msg.(type) {
// Sadly, we can't lump BatchMsg and sequenceMsg together even
// though they are both []Cmd... the compiler will only allow
// casting for one of the known types at a time; in which case, even
// an implicit cast is good enough! (With both types listed in the
// 'case', it assumes it's just a generic Msg.)
case BatchMsg:
cmds = msg
case sequenceMsg:
cmds = msg

default:
// for all other messages, we "simply" wrap the result
return WrappedMsg{Id: id, Msg: msg}
}

// BatchMsg and sequenceMsg are specially-handled by the event loop;
// we need to propagate the wrapping into the individual commands.
wrapped := make([]Cmd, 0, len(cmds))
for _, c := range cmds {
if c == nil {
continue
}
wrapped = append(wrapped, Wrap(c, id))
}
if _, ok := msg.(BatchMsg); ok {
return BatchMsg(wrapped)
}
return sequenceMsg(wrapped)
}
}

// WrappedMsg represents a wrapper around the Msg returned from a Cmd. See Wrap
// for further details.
type WrappedMsg struct {
Id int
Msg Msg
}

// Every is a command that ticks in sync with the system clock. So, if you
// wanted to tick with the system clock every second, minute or hour you
// could use this. It's also handy for having different things tick in sync.
Expand Down
71 changes: 71 additions & 0 deletions commands_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,3 +105,74 @@ func TestBatch(t *testing.T) {
}
})
}

func TestWrap(t *testing.T) {
t.Run("wrapped nil cmd", func(t *testing.T) {
if w := Wrap(nil, 1); w != nil {
t.Fatalf("expected nil, got %+v", w)
}
})
t.Run("wrapped single cmd", func(t *testing.T) {
if w := Wrap(Quit, 1); w != nil {
if m, ok := w().(WrappedMsg); !ok {
t.Fatalf("expected WrappedMsg, got %+v", m)
} else if m.Id != 1 {
t.Fatalf("expected WrappedMsg{Id:1}, got %+v", m.Id)
} else if _, ok := m.Msg.(QuitMsg); !ok {
t.Fatalf("expected WrappedMsg{Msg:QuitMsg}, got %+v", m.Msg)
}
} else {
t.Fatal("expected non-nil")
}
})
t.Run("wrapped batch cmd", func(t *testing.T) {
if w := Wrap(Batch(Quit, Quit), 1); w != nil {
switch b := w().(type) {
case BatchMsg:
if l := len(b); l != 2 {
t.Fatalf("expected a []Cmd with length 2, got %d", l)
} else {
// check *each* of the inner commands...
for i, c := range b {
if m, ok := c().(WrappedMsg); !ok {
t.Fatalf("expected WrappedMsg for %d, got %+v", i, m)
} else if m.Id != 1 {
t.Fatalf("expected WrappedMsg{Id:1} for %d, got %+v", i, m.Id)
} else if _, ok := m.Msg.(QuitMsg); !ok {
t.Fatalf("expected WrappedMsg{Msg:QuitMsg} for %d, got %+v", i, m.Msg)
}
}
}
default:
t.Fatalf("expected BatchMsg, got %#v", b)
}
} else {
t.Fatal("expected non-nil")
}
})
t.Run("wrapped sequence cmd", func(t *testing.T) {
if w := Wrap(Sequence(Quit, Quit), 1); w != nil {
switch b := w().(type) {
case sequenceMsg:
if l := len(b); l != 2 {
t.Fatalf("expected a []Cmd with length 2, got %d", l)
} else {
// check *each* of the inner commands...
for i, c := range b {
if m, ok := c().(WrappedMsg); !ok {
t.Fatalf("expected WrappedMsg for %d, got %+v", i, m)
} else if m.Id != 1 {
t.Fatalf("expected WrappedMsg{Id:1} for %d, got %+v", i, m.Id)
} else if _, ok := m.Msg.(QuitMsg); !ok {
t.Fatalf("expected WrappedMsg{Msg:QuitMsg} for %d, got %+v", i, m.Msg)
}
}
}
default:
t.Fatalf("expected sequenceMsg, got %#v", b)
}
} else {
t.Fatal("expected non-nil")
}
})
}

0 comments on commit 4420ef4

Please sign in to comment.