From 88238b097fc8159a87eb1407b1dc8d6f3ed45228 Mon Sep 17 00:00:00 2001 From: Blai Peidro Date: Mon, 19 Oct 2026 02:34:20 +0200 Subject: [PATCH] fix: keep the language in the steps, and go back to it with Esc The language, asked before an installation is looked for, stays at the left once answered, in the install and the upgrade, and Esc on the release notes asks it again. --- internal/cli/cli.go | 3 ++ internal/cli/install.go | 20 ++++++++--- internal/cli/install_test.go | 64 ++++++++++++++++++++++++++++++++++++ internal/cli/upgrade.go | 30 +++++++++++++---- 4 files changed, 106 insertions(+), 11 deletions(-) create mode 100644 internal/cli/install_test.go diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 73ed811..1163a22 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -20,6 +20,9 @@ type Flags struct { InstallerVersion string Lang, Version, Role, Mode, Runtime string + // LangAsked : the language was asked in this run (not given with --lang), so its step stays + // at the left once answered + LangAsked bool InstallRuntime bool Dir, Name string Port int diff --git a/internal/cli/install.go b/internal/cli/install.go index b02012e..af8478b 100644 --- a/internal/cli/install.go +++ b/internal/cli/install.go @@ -28,6 +28,8 @@ type wstep struct { label string // skip : not asked (a flag answered it, or the role or the mode has no such step) skip func() bool + // shown : at the left even when skipped (the language, asked before the wizard began) + shown func() bool // ask asks and keeps the answer ; ui.ErrBack goes to the step before ask func() error } @@ -43,21 +45,26 @@ type wstep struct { // // ErrAborted when stopped, or a step's own error. func runWizard(list []wstep) error { + // back : the step is reached with Esc, so one answered before the wizard began (shown at + // the left though skipped, the language) is asked again + back := false for i := 0; i < len(list); { ui.Steps(visibleLabels(list)...) s := list[i] - if s.skip != nil && s.skip() { + if s.skip != nil && s.skip() && !(back && s.shown != nil && s.shown()) { i++ continue } + back = false ui.Section(stepName(s.label)) err := s.ask() if errors.Is(err, ui.ErrBack) { j := i - 1 - for j > 0 && list[j].skip != nil && list[j].skip() { + for j > 0 && list[j].skip != nil && list[j].skip() && (list[j].shown == nil || !list[j].shown()) { j-- } i = max(0, j) + back = true continue } if err != nil { @@ -69,7 +76,8 @@ func runWizard(list []wstep) error { return nil } -// visibleLabels are the steps at the left : those not skipped, each once, in order. +// visibleLabels are the steps at the left : those not skipped, or shown anyway, each once, in +// order. // // Args: // @@ -82,7 +90,8 @@ func visibleLabels(list []wstep) []string { var out []string seen := map[string]bool{} for _, s := range list { - if (s.skip != nil && s.skip()) || seen[s.label] { + hidden := s.skip != nil && s.skip() && (s.shown == nil || !s.shown()) + if hidden || seen[s.label] { continue } seen[s.label] = true @@ -138,6 +147,7 @@ func install(ctx context.Context, f Flags) error { return err } f.Lang = i18n.Current() + f.LangAsked = true } if st, err := state.Load(); err == nil { ui.Warn(i18n.T("install.already", "version", st.Version, "mode", string(st.Mode))) @@ -182,7 +192,7 @@ func install(ctx context.Context, f Flags) error { var list []wstep list = []wstep{ // first the language, the server's locale chosen ; its steps renamed once changed - {label: "language", skip: func() bool { return ui.Unattended || f.Lang != "" }, ask: func() error { + {label: "language", skip: func() bool { return ui.Unattended || f.Lang != "" }, shown: func() bool { return f.LangAsked }, ask: func() error { return chooseLanguage(func() { ui.Relabel(visibleLabels(list)...) }) }}, // then what this version brings : its breaking changes, removals and deprecations on top diff --git a/internal/cli/install_test.go b/internal/cli/install_test.go new file mode 100644 index 0000000..9b91d51 --- /dev/null +++ b/internal/cli/install_test.go @@ -0,0 +1,64 @@ +package cli + +import ( + "reflect" + "testing" + + "github.com/ansibleforms/installer/internal/ui" +) + +// TestVisibleLabelsKeepsAnsweredLanguage checks that the language, asked before the wizard +// began (so its own step is skipped), stays at the left, while a step skipped for another +// reason does not show. +func TestVisibleLabelsKeepsAnsweredLanguage(t *testing.T) { + asked := true + skipped := func() bool { return true } + list := []wstep{ + {label: "language", skip: skipped, shown: func() bool { return asked }}, + {label: "notes"}, + {label: "folder", skip: skipped}, + {label: "role"}, + } + want := []string{stepName("language"), stepName("notes"), stepName("role")} + if got := visibleLabels(list); !reflect.DeepEqual(got, want) { + t.Fatalf("visibleLabels = %v, want %v", got, want) + } + // given with --lang : never asked, so not at the left + asked = false + want = []string{stepName("notes"), stepName("role")} + if got := visibleLabels(list); !reflect.DeepEqual(got, want) { + t.Fatalf("visibleLabels with --lang = %v, want %v", got, want) + } +} + +// TestRunWizardBackToAnsweredLanguage checks that Esc on the step after the language, asked +// before the wizard began, asks the language again, then goes on. +func TestRunWizardBackToAnsweredLanguage(t *testing.T) { + var asked []string + notesSeen := 0 + list := []wstep{ + {label: "language", skip: func() bool { return true }, shown: func() bool { return true }, ask: func() error { + asked = append(asked, "language") + return nil + }}, + {label: "notes", ask: func() error { + asked = append(asked, "notes") + notesSeen++ + if notesSeen == 1 { + return ui.ErrBack + } + return nil + }}, + {label: "role", ask: func() error { + asked = append(asked, "role") + return nil + }}, + } + if err := runWizard(list); err != nil { + t.Fatal(err) + } + want := []string{"notes", "language", "notes", "role"} + if !reflect.DeepEqual(asked, want) { + t.Fatalf("asked %v, want %v", asked, want) + } +} diff --git a/internal/cli/upgrade.go b/internal/cli/upgrade.go index f195e20..08639fd 100644 --- a/internal/cli/upgrade.go +++ b/internal/cli/upgrade.go @@ -54,6 +54,8 @@ func upgrade(ctx context.Context, f Flags) error { if err := chooseLanguage(func() { ui.Relabel(langLabels()...) }); err != nil { return err } + f.Lang = i18n.Current() + f.LangAsked = true } if st == nil { if st, err = offerFound(ctx, f, found, false); err != nil { @@ -86,25 +88,41 @@ func upgrade(ctx context.Context, f Flags) error { labels := func() []string { l := []string{stepName("version"), stepName("notes"), stepName("upgrade")} - if askLang { + // asked here, or by the install before it came to this upgrade + if f.LangAsked { l = append([]string{stepName("language")}, l...) } return l } ui.Steps(labels()...) - ui.Section(stepName("version")) - ui.Answered(from.String() + " → " + to.String()) - ui.Section(stepName("notes")) entries, err := releases.FetchChangelog(ctx, to) if err != nil { ui.Warn(err.Error()) } skipped := releases.Between(entries, from, to) - if len(skipped) > 0 { + for { + ui.Section(stepName("version")) + ui.Answered(from.String() + " → " + to.String()) + ui.Section(stepName("notes")) + if len(skipped) == 0 { + break + } ui.Explain(i18n.T("upgrade.notesHelp", "from", from.String(), "to", to.String())) - if err := ui.Document(i18n.T("upgrade.notesTitle", "count", len(skipped)), func() { showNotes(skipped) }); err != nil && !errors.Is(err, ui.ErrBack) { + err := ui.Document(i18n.T("upgrade.notesTitle", "count", len(skipped)), func() { showNotes(skipped) }) + if errors.Is(err, ui.ErrBack) && f.LangAsked { + // Esc : back to the language, asked in this run, then the notes again in it + ui.Section(stepName("language")) + if err := chooseLanguage(func() { ui.Relabel(labels()...) }); err != nil && !errors.Is(err, ui.ErrBack) { + return err + } + f.Lang = i18n.Current() + ui.Steps(labels()...) + continue + } + if err != nil && !errors.Is(err, ui.ErrBack) { return err } + break } // a Runner of 7.2 in database mode : its runner, its app and its mode asked and reviewed, // then moved to the installer's Runner layout (migrate.go)