diff --git a/internal/cli/bumpver.go b/internal/cli/bumpver.go index 400aadcd..53ffc6d7 100644 --- a/internal/cli/bumpver.go +++ b/internal/cli/bumpver.go @@ -76,29 +76,31 @@ func runBumpVer(cmd *cobra.Command, opts bumpVerOpts) error { return err } - changelog, err := changelog.RunChangelog() + changelogModel, err := changelog.RunChangelog(cmd.InOrStdin(), cmd.OutOrStdout()) if err != nil { return err } - switch changelog.Increment { - case "patch": + switch changelogModel.Increment { + case changelog.Patch: newVer = currentVer.IncPatch() - case "minor": + case changelog.Minor: newVer = currentVer.IncMinor() - case "major": + case changelog.Major: newVer = currentVer.IncMajor() } - changelogMsg = changelog.Msg + changelogMsg = changelogModel.Msg } else { optVer, err := semver.NewVersion(opts.RecipeVersion) if err != nil { - if errors.Is(err, semver.ErrInvalidSemVer) { + switch { + case errors.Is(err, semver.ErrInvalidSemVer): return fmt.Errorf("provided version is not valid semver: %s", opts.RecipeVersion) + default: + return err } - return err } newVer = *optVer @@ -115,8 +117,7 @@ func runBumpVer(cmd *cobra.Command, opts bumpVerOpts) error { return err } - cmd.Printf("bumped version: %s => %s \n", prevVer, newVerWithPrefix) - cmd.Printf("with changelog message: %s \n", changelogMsg) + cmd.Printf("Recipe version bumped: %s => %s \n", prevVer, newVerWithPrefix) return nil } diff --git a/pkg/ui/changelog/changelog.go b/pkg/ui/changelog/changelog.go index 7bc04952..3e8b5aeb 100644 --- a/pkg/ui/changelog/changelog.go +++ b/pkg/ui/changelog/changelog.go @@ -3,9 +3,16 @@ package changelog import ( "errors" "fmt" + "io" tea "github.com/charmbracelet/bubbletea" - changelog "github.com/futurice/jalapeno/pkg/ui/changelog/prompt" + "github.com/futurice/jalapeno/pkg/ui/util" +) + +const ( + Patch = "patch" + Minor = "minor" + Major = "major" ) type Changelog struct { @@ -13,14 +20,14 @@ type Changelog struct { Msg string } -func RunChangelog() (Changelog, error) { - verInc, err := runSelectPrompt() +func RunChangelog(in io.Reader, out io.Writer) (Changelog, error) { + verInc, err := runVersionPrompt(in, out) if err != nil { return Changelog{}, fmt.Errorf("failed to get version type: %w", err) } - logmsg, err := runTextAreaPrompt() + logmsg, err := runMessagePrompt(in, out) if err != nil { return Changelog{}, fmt.Errorf("failed to get log message: %w", err) @@ -34,32 +41,43 @@ func RunChangelog() (Changelog, error) { return changelog, nil } -func runSelectPrompt() (string, error) { - options := []string{"patch", "minor", "major"} +func runVersionPrompt(in io.Reader, out io.Writer) (string, error) { + options := []string{Patch, Minor, Major} - p := tea.NewProgram(changelog.NewSelectModel(options)) + p := tea.NewProgram(NewSelectModel(options), tea.WithInput(in), tea.WithOutput(out)) if m, err := p.Run(); err != nil { return "", err } else { - sel, ok := m.(changelog.SelectModel) + sel, ok := m.(VersionModel) if !ok { return "", errors.New("internal error: unexpected model type") } - return sel.Value(), nil + value := sel.Value() + if value == "" { + return "", util.ErrUserAborted + } + + return value, nil } } -func runTextAreaPrompt() (string, error) { - p := tea.NewProgram(changelog.NewStringModel()) +func runMessagePrompt(in io.Reader, out io.Writer) (string, error) { + p := tea.NewProgram(NewStringModel(), tea.WithInput(in), tea.WithOutput(out)) if m, err := p.Run(); err != nil { return "", err } else { - txt, ok := m.(changelog.StringModel) + txt, ok := m.(MessageModel) if !ok { return "", errors.New("internal error: unexpected model type") } + + value := txt.Value() + if value == "" { + return "", util.ErrUserAborted + } + return txt.Value(), nil } } diff --git a/pkg/ui/changelog/message_prompt.go b/pkg/ui/changelog/message_prompt.go new file mode 100644 index 00000000..8d729c8f --- /dev/null +++ b/pkg/ui/changelog/message_prompt.go @@ -0,0 +1,93 @@ +package changelog + +import ( + "errors" + "fmt" + "strings" + + "github.com/charmbracelet/bubbles/textarea" + "github.com/charmbracelet/bubbles/textinput" + tea "github.com/charmbracelet/bubbletea" + "github.com/futurice/jalapeno/pkg/ui/colors" + "github.com/muesli/reflow/wordwrap" +) + +type MessageModel struct { + textArea textarea.Model + width int + err error +} + +var _ tea.Model = MessageModel{} + +func NewStringModel() MessageModel { + ti := textarea.New() + ti.Focus() + ti.SetHeight(5) + ti.CharLimit = 156 + + return MessageModel{ + textArea: ti, + err: nil, + } +} + +func (m MessageModel) Init() tea.Cmd { + return textinput.Blink +} + +func (m MessageModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + var cmd tea.Cmd + + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.Type { + case tea.KeyCtrlC, tea.KeyEsc: + return m, tea.Quit + case tea.KeyCtrlS: + m.err = m.Validate() + if m.err == nil { + return m, tea.Quit + } + return m, nil + } + + case tea.WindowSizeMsg: + m.width = msg.Width + m.textArea.SetWidth(m.width) + } + + m.textArea, cmd = m.textArea.Update(msg) + return m, cmd +} + +func (m MessageModel) View() string { + var s strings.Builder + + s.WriteString(wordwrap.String("Write the changelog message for the new version", m.width)) + s.WriteString("\nPress Ctrl+S to save") + s.WriteRune('\n') + + s.WriteString(m.textArea.View()) + + if m.err != nil { + s.WriteString("\n\n") + s.WriteString(colors.Red.Render(fmt.Sprintf("Error: %s", m.err.Error()))) + } + + s.WriteString("\n\n") + + return s.String() +} + +func (m MessageModel) Value() string { + return strings.TrimSpace(m.textArea.Value()) +} + +func (m MessageModel) Validate() error { + if m.textArea.Value() == "" { + return errors.New("changelog message cannot be empty") + } + + return nil +} diff --git a/pkg/ui/changelog/prompt/textarea.go b/pkg/ui/changelog/prompt/textarea.go deleted file mode 100644 index f6034d2a..00000000 --- a/pkg/ui/changelog/prompt/textarea.go +++ /dev/null @@ -1,124 +0,0 @@ -package changelog - -import ( - "strings" - - "github.com/charmbracelet/bubbles/textarea" - "github.com/charmbracelet/bubbles/textinput" - tea "github.com/charmbracelet/bubbletea" - "github.com/futurice/jalapeno/pkg/ui/util" - "github.com/muesli/reflow/wordwrap" -) - -type StringModel struct { - textArea textarea.Model - submitted bool - showDescription bool - width int - err error -} - -var _ tea.Model = StringModel{} - -func NewStringModel() StringModel { - ti := textarea.New() - ti.Focus() - ti.CharLimit = 156 - - return StringModel{ - textArea: ti, - err: nil, - } -} - -func (m StringModel) Init() tea.Cmd { - return textinput.Blink -} - -func (m StringModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var cmd tea.Cmd - - switch msg := msg.(type) { - case tea.KeyMsg: - switch msg.Type { - case tea.KeyCtrlS: - err := m.Validate() - if err != nil { - m.err = err - return m, nil - } - m.submitted = true - return m, tea.Quit - case tea.KeyRunes: - switch string(msg.Runes) { - case "?": - if !m.showDescription { - m.showDescription = true - return m, nil - } - } - } - - case tea.WindowSizeMsg: - m.width = msg.Width - m.textArea.SetWidth(m.width) - } - - m.textArea, cmd = m.textArea.Update(msg) - return m, cmd -} - -func (m StringModel) View() string { - var s strings.Builder - - if m.submitted { - if m.textArea.Value() == "" { - s.WriteString("empty") - } else { - s.WriteString(m.textArea.Value()) - } - - return s.String() - } - - if !m.showDescription { - s.WriteString(" [type ? for more info]") - } - - s.WriteRune('\n') - if m.showDescription { - s.WriteString(wordwrap.String("Changelog message for version bump\npress Ctrl+S to save", m.width)) - s.WriteRune('\n') - } - - s.WriteString(m.textArea.View()) - - if m.err != nil { - s.WriteRune('\n') - errMsg := m.err.Error() - errMsg = strings.ToUpper(errMsg[:1]) + errMsg[1:] - s.WriteString(wordwrap.String(errMsg, m.width)) - } - - return s.String() -} - -func (m StringModel) Name() string { - return "Hello world" -} - -func (m StringModel) Value() string { - return m.textArea.Value() -} - -func (m StringModel) IsSubmitted() bool { - return m.submitted -} - -func (m StringModel) Validate() error { - if m.textArea.Value() == "" { - return util.ErrRequired - } - - return nil -} diff --git a/pkg/ui/changelog/prompt/select.go b/pkg/ui/changelog/version_prompt.go similarity index 64% rename from pkg/ui/changelog/prompt/select.go rename to pkg/ui/changelog/version_prompt.go index 7690baf7..750942fc 100644 --- a/pkg/ui/changelog/prompt/select.go +++ b/pkg/ui/changelog/version_prompt.go @@ -16,15 +16,13 @@ var ( selectedItemStyle = lipgloss.NewStyle().PaddingLeft(0).Foreground(lipgloss.Color("170")) ) -type SelectModel struct { - list list.Model - value string - showDescription bool - submitted bool - width int +type VersionModel struct { + list list.Model + value string + width int } -var _ tea.Model = SelectModel{} +var _ tea.Model = VersionModel{} type selectItem string @@ -55,48 +53,36 @@ func (d selectItemDelegate) Render(w io.Writer, m list.Model, index int, listIte fmt.Fprint(w, fn(string(i))) } -func NewSelectModel(options []string) SelectModel { +func NewSelectModel(options []string) VersionModel { items := make([]list.Item, len(options)) for i := range options { items[i] = selectItem(options[i]) } - const ( - defaultWidth = 20 - defaultHeight = 14 - ) - - l := list.New(items, selectItemDelegate{}, defaultWidth, defaultHeight) + l := list.New(items, selectItemDelegate{}, 6, 5) l.SetShowStatusBar(false) l.SetFilteringEnabled(false) l.SetShowHelp(false) l.SetShowTitle(false) - return SelectModel{ + return VersionModel{ list: l, } } -func (m SelectModel) Init() tea.Cmd { +func (m VersionModel) Init() tea.Cmd { return nil } -func (m SelectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { +func (m VersionModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: switch msg.Type { + case tea.KeyCtrlC, tea.KeyEsc: + return m, tea.Quit case tea.KeyEnter: - m.submitted = true m.value = string(m.list.SelectedItem().(selectItem)) return m, tea.Quit - case tea.KeyRunes: - switch string(msg.Runes) { - case "?": - if !m.showDescription { - m.showDescription = true - return m, nil - } - } } case tea.WindowSizeMsg: @@ -109,27 +95,16 @@ func (m SelectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { return m, cmd } -func (m SelectModel) View() string { +func (m VersionModel) View() string { var s strings.Builder - if m.submitted { - s.WriteString(fmt.Sprintf(": %s", m.value)) - return s.String() - } + s.WriteString(wordwrap.String("Select which version to bump:", m.width)) s.WriteRune('\n') - if m.showDescription { - s.WriteString(wordwrap.String("Select version update type", m.width)) - s.WriteRune('\n') - } - s.WriteString(m.list.View()) + return s.String() } -func (m SelectModel) Value() string { +func (m VersionModel) Value() string { return m.value } - -func (m SelectModel) IsSubmitted() bool { - return m.submitted -} diff --git a/test/features/bumpver.feature b/test/features/bumpver.feature index 6f20ace0..f305f356 100644 --- a/test/features/bumpver.feature +++ b/test/features/bumpver.feature @@ -3,18 +3,20 @@ Feature: Bump recipe version and write changelog Scenario: Directly bump version Given a recipe "foo" When I bump recipe "foo" version to "v0.0.2" with message "Test" - Then recipe "foo" has version "v0.0.2" - And CLI produced an output "bumped version: v0.0.1 => v0.0.2" + Then no errors were printed + And recipe "foo" has version "v0.0.2" + And CLI produced an output "Recipe version bumped: v0.0.1 => v0.0.2" And recipe "foo" has changelog message "Test" Scenario: Command inits changelog Given a recipe "foo" When I bump recipe "foo" version to "v0.0.2" with message "Test" - Then recipe "foo" contains changelog with 2 entries - And CLI produced an output "bumped version: v0.0.1 => v0.0.2" + Then no errors were printed + And recipe "foo" contains changelog with 2 entries + And CLI produced an output "Recipe version bumped: v0.0.1 => v0.0.2" And first entry in recipe "foo" changelog has message "Init version" Scenario: Invalid semantic version Given a recipe "foo" - When I bump recipe "foo" version to "no-valid-semver" with message "Test" + When I bump recipe "foo" version to "not-valid-semver" with message "Test" Then CLI produced an error "Error: provided version is not valid semver"