2 Commits
Author SHA1 Message Date
Michael WesemannandClaude Opus 5 c22a66c470 [mike@mwxm4]
The customisation picker opens on the site's own road.

A domain, a netmask and a gateway in ~/.gvmrc are written down so that machines
get them — that is what writing them down was for — so the line that gives them
to the guest is where the cursor starts, rather than the one that leaves the
guest as the template made it:

      leave the guest as the template made it
    ▸ this site — fhi-berlin.mpg.de, gateway 10.0.0.1
      linux-static
      windows-domain

Where no site is set up it starts at the top as before, on the answer that
cannot be wrong. Nothing is decided by where the cursor sits: the picker picks
on Enter, the address is asked for after it, and the confirmation still says in
full what will happen before anything is made.

What the picker offers is a function of its own now (customChoices), which is
what made it testable: the lines, what each of them means, and where to open
are one answer to one question and were three statements spread through a
method that also asks four others.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 14:57:33 +02:00
Michael WesemannandClaude Opus 5 ae936420e0 [mike@mwxm4]
The address question comes up with "auto" in it.

A site with an address tool hands addresses out from it — that is what the tool
is configured for — so typing the word every time was asking somebody to
confirm the ordinary case by hand. It stands in the line now, and Enter is the
whole answer:

    address for web05, "auto" for one from dns, empty for DHCP: auto

Offered rather than made the meaning of the empty line. Every answer keeps the
meaning it had: erased back to nothing it is DHCP again, or whatever the chosen
specification says, and an address typed by hand is still typed by hand. Enter
commits nothing either — what is fetched here is handed straight back when the
deployment is not confirmed.

The prefilled line is input's own doing (inputWith), with the cursor at the end
of it, so it is a default that can be seen. One that cannot be seen is a
question answered by what was not typed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 14:53:42 +02:00
5 changed files with 147 additions and 28 deletions
+22 -8
View File
@@ -545,16 +545,23 @@ are open —
customise web05 how v308
▸ leave the guest as the template made it
this site — fhi-berlin.mpg.de, gateway 10.0.0.1
leave the guest as the template made it
▸ this site — fhi-berlin.mpg.de, gateway 10.0.0.1
linux-static
windows-domain
— and then one line for the address, which says in words what leaving it empty
would do:
The cursor starts on gvm's own road wherever the site is set up: a domain, a
netmask and a gateway written into `~/.gvmrc` are written down so that machines
get them, and the answer wanted nearly every time should not be the one that has
to be arrowed to. Where the site is not set up it starts at the top instead, on
the answer that is never wrong. Nothing is decided by where it sits — the picker
picks on Enter, and the confirmation still says in full what will happen.
address for web05, or empty for DHCP:
address for web05, or empty to leave it to linux-static:
Then one line for the address, which says in words what leaving it empty would
do:
address for web05, empty for DHCP:
address for web05, empty to leave it to linux-static:
Where that picker does **not** appear, the confirmation says why rather than
leaving a step to look broken — neither road is open, because the vCenter holds
@@ -571,9 +578,16 @@ when it will not say.
gvm new --from ubuntu-tpl --name web05 --ip auto
Where the site has a tool that hands addresses out, `--ip auto` asks it instead
of making you look one up first. In the list the same word does it:
of making you look one up first. In the list the same word does it, and it is
already standing in the line — a site with a tool hands addresses out from it,
so that is the answer Enter gives:
address for web05, "auto" for one from dns, empty for DHCP:
address for web05, "auto" for one from dns, empty for DHCP: auto
It is offered rather than made the meaning of the empty line, so every answer
keeps the meaning it had: erased back to nothing it is DHCP again, or whatever
the chosen specification says. And Enter commits nothing — an address fetched
here is handed straight back when the deployment is not confirmed.
It shells out to `dns`, the Infoblox helper — found on the path by that name, or
named as `dnstool` in `~/.gvmrc` where it lives somewhere else. Everything it
+67 -17
View File
@@ -372,7 +372,19 @@ func (b *browser) openPicker(r vmRow, kind pickKind) {
// callback runs after the picker has closed, so that what it does — a
// confirmation, another question — has the screen to itself.
func (b *browser) choose(title string, r vmRow, lines []string, chosen func(int)) {
b.pick = &picker{title: title, row: r, lines: lines, chosen: chosen}
b.chooseAt(title, r, lines, 0, chosen)
}
// chooseAt is the same with the cursor starting somewhere other than the top,
// for a list whose ordinary answer is not its first line. Where the cursor
// starts is the only default a picker has — nothing is picked until Enter — so
// it belongs on the line somebody nearly always wants, and the others are one
// key away.
func (b *browser) chooseAt(title string, r vmRow, lines []string, at int, chosen func(int)) {
if at < 0 || at >= len(lines) {
at = 0
}
b.pick = &picker{title: title, row: r, lines: lines, chosen: chosen, sel: at}
}
func (b *browser) closePicker() { b.pick = nil }
@@ -677,21 +689,9 @@ func (b *browser) deploy(r vmRow) {
return
}
lines := []string{"leave the guest as the template made it"}
kinds := []string{""} // what each line means: "" none, "-" gvm's own, else a name
if own {
// What it will do, not where the settings are kept: a picker line that
// is mostly an absolute path says nothing about the choice being made,
// and `gvm config` is where the file is named.
lines = append(lines, SF("this site — %s, gateway %s", b.site.domain, b.site.gateway))
kinds = append(kinds, "-")
}
for _, name := range specs {
lines = append(lines, name)
kinds = append(kinds, name)
}
lines, kinds, at := customChoices(b.site, specs, own)
b.choose(SF("customise %s how", name), r, lines, func(i int) {
b.chooseAt(SF("customise %s how", name), r, lines, at, func(i int) {
switch kinds[i] {
case "":
b.deployAsk(r, src, target, name, deployOpts{}, deployStep{})
@@ -705,13 +705,50 @@ func (b *browser) deploy(r vmRow) {
})
}
// customChoices is what the customisation picker offers, what each line means,
// and where its cursor starts.
//
// kinds is the meaning, carried beside the lines rather than worked out from an
// index: "" leaves the guest alone, "-" is the specification gvm writes from
// ~/.gvmrc, and anything else is the name of one the vCenter holds.
//
// The cursor starts on gvm's own road where the site is set up. A site whose
// domain, netmask and gateway have been written down is a site whose machines
// are meant to be given them — that is what writing them down was for — and the
// answer wanted nearly every time should not be the one that has to be arrowed
// to. Where there is no such road it starts at the top, on the answer that is
// never wrong: leave the guest as the template made it.
//
// Nothing is decided by the cursor sitting there. The picker chooses on Enter,
// the address is asked for after it, and the confirmation still says in full
// what will happen.
func customChoices(st site, specs []string, own bool) (lines, kinds []string, at int) {
lines = []string{"leave the guest as the template made it"}
kinds = []string{""}
if own {
// What it will do, not where the settings are kept: a picker line that
// is mostly an absolute path says nothing about the choice being made,
// and `gvm config` is where the file is named.
lines = append(lines, SF("this site — %s, gateway %s", st.domain, st.gateway))
kinds = append(kinds, "-")
at = len(lines) - 1
}
for _, name := range specs {
lines = append(lines, name)
kinds = append(kinds, name)
}
return lines, kinds, at
}
// deployAddress asks for the address, which is the other half of what a
// specification cannot know: it holds the netmask, the gateway and the domain,
// and the machine holds its own number.
//
// Empty is an answer: it leaves whatever the specification says, which is
// usually DHCP, and that is the ordinary case on a network that hands out
// addresses.
// addresses. Where the site has a tool of its own, the ordinary case is the
// other one — so "auto" is in the line when the question appears, and Enter
// takes it.
func (b *browser) deployAddress(r vmRow, src deploySource, t deployTarget, name string, opts deployOpts) {
// Empty is an answer, and it means two different things: with gvm's own
// specification it leaves the adapter on DHCP, and with one of the
@@ -728,11 +765,24 @@ func (b *browser) deployAddress(r vmRow, src deploySource, t deployTarget, name
leave = "empty to leave it to " + opts.spec
}
ask := SF("address for %s, %s: ", name, leave)
// And where there is a tool, its word is standing in the line already, so
// Enter is the whole answer. That is what the tool is configured for: a
// site that has one hands out addresses from it, and typing "auto" every
// time is asking somebody to confirm the ordinary case by hand.
//
// Offered rather than made the meaning of the empty line, so that every
// answer keeps the meaning it had: erased back to nothing it is DHCP again,
// or whatever the specification says. Nor does pressing Enter commit
// anything — an address fetched here is handed straight back where the
// deployment is not confirmed (deployStep.giveBack).
preset := ""
if opts.dns.there() {
ask = SF("address for %s, %q for one from dns, %s: ", name, autoIP, leave)
preset = autoIP
}
ip, ok := b.input(ask)
ip, ok := b.inputWith(ask, preset)
if !ok {
b.setStatus(colDim, "nothing done")
return
+12 -2
View File
@@ -1300,8 +1300,18 @@ func (e *editor) key(k key) (finished, accepted bool) {
// input reads one line in the status area and reports whether it was finished
// rather than abandoned. It has its own key loop, so nothing that is typed here
// reaches the filter.
func (b *browser) input(label string) (string, bool) {
b.edit = &editor{label: label}
func (b *browser) input(label string) (string, bool) { return b.inputWith(label, "") }
// inputWith is the same with an answer already standing in the line, for a
// question that has one ordinary answer worth offering: Enter takes it, and
// Backspace is how it is refused. It is a default one can see — which is the
// only kind worth having, since a question whose answer is decided by what was
// not typed is one nobody knows they have agreed to.
//
// The cursor sits at the end of it, so typing carries on from the answer rather
// than into the middle of it.
func (b *browser) inputWith(label, preset string) (string, bool) {
b.edit = &editor{label: label, runes: []rune(preset), cursor: len([]rune(preset))}
defer func() { b.edit = nil }()
for {
+45
View File
@@ -571,6 +571,43 @@ func TestTheSiteComesOutOfTheConfiguration(t *testing.T) {
}
}
// The picker opens on the road gvm writes itself where the site has been set
// up: a domain, a netmask and a gateway in ~/.gvmrc are there so that machines
// get them, and the answer wanted nearly every time must not be the one that
// has to be arrowed to. Where there is no such road it opens at the top, on the
// answer that is never wrong.
func TestTheCustomisationPickerOpensOnTheSiteRoad(t *testing.T) {
st := testSite()
lines, kinds, at := customChoices(st, []string{"linux-static", "windows"}, true)
if len(lines) != 4 || len(kinds) != len(lines) {
t.Fatalf("the picker offers %d lines and %d meanings: %v", len(lines), len(kinds), lines)
}
if kinds[at] != "-" {
t.Errorf("it opens on %q, which is %q — want gvm's own road", lines[at], kinds[at])
}
// And that line says what it will do, in the site's own words: a default
// nobody can read is one nobody knows they are taking.
if !strings.Contains(lines[at], st.domain) || !strings.Contains(lines[at], st.gateway) {
t.Errorf("the line it opens on is %q, want the domain and the gateway in it", lines[at])
}
// The vCenter's own are still there, after it and in order.
if lines[2] != "linux-static" || lines[3] != "windows" {
t.Errorf("the vCenter's specifications came out as %v", lines[2:])
}
// No site, and the cursor is at the top rather than on somebody else's
// specification: leaving the guest as the template made it is the one
// answer that cannot be wrong.
lines, kinds, at = customChoices(site{}, []string{"linux-static"}, false)
if at != 0 || kinds[at] != "" {
t.Errorf("with no site it opens on %q (%q)", lines[at], kinds[at])
}
if len(lines) != 2 {
t.Errorf("with no site the picker offers %v", lines)
}
}
// The question that asks for an address has to read as a sentence on both
// roads. It did not: the interactive half settles which road before it asks,
// so at that moment every field is still empty — which read as "no
@@ -581,6 +618,8 @@ func TestTheAddressQuestionReadsAsASentence(t *testing.T) {
t.Setenv("COLUMNS", "100")
t.Setenv("LINES", "24")
tool, _ := fakeInfoblox(t) // for its there(), not for its answers
for _, c := range []struct {
what string
opts deployOpts
@@ -589,6 +628,12 @@ func TestTheAddressQuestionReadsAsASentence(t *testing.T) {
{"gvm's own specification", deployOpts{how: customSite, st: testSite()}, "empty for DHCP"},
{"one the vCenter holds", deployOpts{how: customSpec, spec: "linux-static"},
"leave it to linux-static"},
// With an address tool, the answer it would give is standing in the
// line already: that is what the tool is there for, and the question
// still has to say what the other two answers mean.
{"a site with an address tool",
deployOpts{how: customSite, st: testSite(), dns: tool},
`"auto" for one from dns, empty for DHCP: auto`},
} {
r := templateRow("ubuntu-tpl")
b := &browser{rows: []vmRow{r}, view: []int{0}}
+1 -1
View File
@@ -1 +1 @@
1.3.8
1.3.10