2 Commits
Author SHA1 Message Date
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
Michael WesemannandClaude Opus 5 1aab0cf4a5 [mike@mwxm4]
Straight to the list, live, once a machine is being made.

Starting a clone left a sentence on the template's sheet saying that its TASK
column shows how far along it is and that ^l would make that move — three steps
of housekeeping after the one decision that mattered, on a screen which is a
page of facts about the machine being copied rather than about the one being
made.

Now the sheet goes away, the list comes back with the cursor still on the row
the task hangs off, and live mode turns itself on where it was off. The
percentage moves by itself, the new machine turns up in the table when it
exists, and the line under the table announces it.

    web05 is being made at 141.14.140.182 — live on, ubuntu-tpl shows how far
    along it is

Live is said in that line because it is a mode and it stays on: the title says
it is running and ^l ends it. It does not sweep at once the way ^l does — the
deployment re-read its own row a moment earlier — so the first tick is one
interval away, and that interval is already the busy one, because the row it
re-read is the one carrying the clone.

Putting the sheet away is a function of its own now: Esc and this both have to
forget the events that were read during the visit, and that reason belongs in
one place.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 14:41:38 +02:00
8 changed files with 182 additions and 23 deletions
+26 -7
View File
@@ -265,6 +265,10 @@ It refreshes over the connections that are already open. `^r` logs in again,
which is how a session that has died is recovered; doing that every ten seconds
would be three logins a minute for nothing.
`^l` is not the only way in: making a machine from a template turns it on, since
a clone that has just been started is exactly the screen this is for, and says
so in the same line that reports the machine.
Two things come with it.
**The line under the table says what just changed.** A table shows what is; this
@@ -479,10 +483,17 @@ puts it somewhere other than the template's own.
**It does not wait.** A clone copies every disk the template has, which is
minutes to the half hour, and a list frozen for that long is a list nobody would
start one from. The task hangs off the template in vCenter, so the row it was
started from shows how far along it is in its TASK column — `^l` makes that move
on its own, and announces the new machine on the line under the table when it
exists. `gvm new` on the command line does wait: a script that gets its prompt
back wants the machine to be there.
started from shows how far along it is in its TASK column. `gvm new` on the
command line does wait: a script that gets its prompt back wants the machine to
be there.
**And it goes to that column.** The machine's sheet — which is where the menu
was opened and a page of facts about the *template* — is put away, the list
comes back with the cursor still on the row the clone hangs off, and live mode
turns itself on if it was off, so the percentage moves on its own and the line
under the table announces the new machine when it exists. `^l` ends it again.
Naming the key that would make an invisible figure move is three steps of
housekeeping after the one decision that mattered.
The rest of a template's menu is greyed out with "a template" next to it, because
vSphere will not start one, snapshot one or reconfigure one. They are greyed
@@ -560,9 +571,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
@@ -586,7 +604,8 @@ answer short of the same answer.
the line that is left on the screen afterwards — this is a number somebody
writes down:
web05 is being made at 141.14.140.182 — ubuntu-tpl shows how far along it is
web05 is being made at 141.14.140.182 — live on, ubuntu-tpl shows how far
along it is
Three more things about it are deliberate:
+43 -5
View File
@@ -594,8 +594,14 @@ func sizePrompt(r vmRow, sz sizing, k sizeKind) string {
// minutes to the half hour, and a list that is frozen for that long is a list
// nobody will start one from. vCenter carries the task either way, and it hangs
// off the template — so the row this was started from shows how far along it is
// in its TASK column, and ^l makes that move on its own. The machine itself
// turns up in the list when it exists, which live mode announces.
// in its TASK column. The machine itself turns up in the list when it exists,
// which live mode announces.
//
// Which is why the end of it is a screen and not only a sentence: the sheet
// goes away, the list comes back with the cursor still on the template, and
// live mode turns itself on. Telling somebody that a figure they cannot see
// exists, and which key would make it move, is three steps of housekeeping
// after the one decision that mattered.
//
// The question at the end is the plain one. Nothing is lost here; something is
// made, and what has to be read before it is made is *where* — which is why
@@ -705,7 +711,9 @@ func (b *browser) deploy(r vmRow) {
//
// 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
@@ -722,11 +730,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
@@ -795,13 +816,30 @@ func (b *browser) deployAsk(r vmRow, src deploySource, t deployTarget, name stri
b.setStatus(colErr, err.Error())
return
}
// And then the screen it can be watched on. This was started from the
// template's sheet — a page of facts about the machine being copied, which
// is not what anybody is here for now. The list is: the clone's progress
// stands in the TASK column of the row the cursor is already on, the new
// machine turns up there when it exists, and the line under the table says
// so. Live mode goes with it, or the percentage would sit where it was
// until somebody pressed a key.
//
// Decided before done, which is what puts the task on the row, and applied
// after it, so that the interval it schedules is the busy one.
watching := ""
if !b.live {
watching = "live on, "
}
b.closeDetail()
// The address is in the line that survives, not only in the question that
// was answered a moment ago: it is the one thing here somebody writes down.
at := ""
if opts.ip != "" && opts.customising() {
at = " at " + opts.ip
}
b.done(SF("%s is being made%s — %s shows how far along it is", name, at, src.name))
b.done(SF("%s is being made%s — %s%s shows how far along it is", name, at, watching, src.name))
b.watchLive()
}
// working puts a line on the screen before an operation that will block the loop
+26 -9
View File
@@ -1012,13 +1012,7 @@ func (b *browser) detailKey(k key) {
switch k.special {
case keyEsc, keyEnter, keyBackspace, keyLeft:
b.detail, b.dscroll = nil, 0
// The events go with the visit, not with the machine: coming back to a
// sheet half an hour later and finding half-hour-old events under a
// label that says nothing about when they were read would be the one
// stale thing on an otherwise freshly read screen. They are one
// keystroke away again.
b.events, b.eventsOf = nil, ""
b.closeDetail()
b.setStatus("", "")
case keyCtrlA:
b.openMenu()
@@ -1306,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 {
@@ -1717,6 +1721,19 @@ func (b *browser) openDetail() {
b.dscroll = 0
}
// closeDetail puts the sheet away and leaves the list underneath it. It is what
// Esc does, and also what an action does when what happens next is to be
// watched in the table rather than read on one machine's page (actions.go).
//
// The events go with the visit, not with the machine: coming back to a sheet
// half an hour later and finding half-hour-old events under a label that says
// nothing about when they were read would be the one stale thing on an
// otherwise freshly read screen. They are one keystroke away again.
func (b *browser) closeDetail() {
b.detail, b.dscroll = nil, 0
b.events, b.eventsOf = nil, ""
}
// eventSheet is the history as sheet lines: the label on the first, each line in
// the colour of its own severity. It is appended to the sheet rather than built
// into vmDetail because vmDetail asks nothing of the network and this is the one
+23
View File
@@ -380,6 +380,29 @@ func TestTheSheetClosesWhenItsMachineGoes(t *testing.T) {
}
}
// Closing the sheet leaves nothing of the visit behind. The events are the part
// that would keep: they are read once, on request, and a sheet opened again
// half an hour later must not show them under a label that says nothing about
// when they were read. Esc does this, and so does a deployment, which puts the
// sheet away to leave the list watching the clone (actions.go).
func TestClosingTheSheetForgetsTheVisit(t *testing.T) {
b := testBrowser("ubuntu-tpl", "web01")
b.applySort()
b.events = []eventLine{{text: "08.09. 11:41 Cannot connect to host esx03", col: colFull}}
b.eventsOf = b.current().id()
b.openDetail()
b.dscroll = 4
b.closeDetail()
if b.detail != nil || b.dscroll != 0 {
t.Errorf("the sheet is still there: %d lines, scrolled to %d", len(b.detail), b.dscroll)
}
if b.events != nil || b.eventsOf != "" {
t.Errorf("the events of %q were kept: %v", b.eventsOf, b.events)
}
}
func TestDetailSheetHasTheParameters(t *testing.T) {
sheet := sheetText(vmDetail(testRow("web01", true, "10.0.0.5"), []string{"none"}, colOff))
+8
View File
@@ -581,6 +581,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 +591,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}}
+24 -1
View File
@@ -3,7 +3,9 @@
// Everything else in the interactive half happens because somebody pressed a
// key. This is the part that happens because time passed: `^l` turns it on and
// the list re-reads itself every few seconds, which turns gvm from something one
// looks at into something one leaves open on a second screen.
// looks at into something one leaves open on a second screen. Starting a clone
// turns it on as well (watchLive), because that is a screen made to be watched
// whether or not anybody thought to ask for it first.
//
// Three things come with it, and they are here rather than in browse.go because
// they only mean anything together:
@@ -69,6 +71,27 @@ func (b *browser) toggleLive() {
b.setStatus(colBusy, "live on — the list re-reads itself; ^l off")
}
// watchLive turns live mode on because something with a figure to watch was
// just set going, and reports whether it had to. ^l is somebody asking for a
// screen that keeps itself up to date; this is gvm deciding that a clone it has
// just started makes the list one of those, since the progress in the TASK
// column is the whole reason to still be looking at it. It stays on afterwards,
// like the mode it is — ^l ends it, and the title says it is running.
//
// Unlike toggleLive it does not sweep at once. The caller has just re-read the
// row it acted on, so the first tick belongs one interval away rather than now
// — and that interval is already the busy one, because that row is the one
// carrying the task.
func (b *browser) watchLive() bool {
if b.live {
return false
}
b.live = true
b.liveGap = b.liveInterval()
b.liveNext = time.Now().Add(b.liveGap)
return true
}
// liveReady reports whether a tick may happen now. Only the list and a
// machine's sheet are refreshed underneath somebody: the menu decides what it
// offers from the state it was drawn with, the picker holds a list of snapshots
+31
View File
@@ -485,3 +485,34 @@ func TestToggleLiveSaysSoAndLooksNow(t *testing.T) {
t.Errorf("turning it off said %q", b.status)
}
}
// A clone that has just been started makes the list a screen worth watching, so
// live mode turns itself on rather than leaving a percentage that moves only
// when somebody remembers ^l. The first tick is an interval away rather than
// now — the action re-read its own row a moment ago — and that interval is the
// busy one, because the row it re-read is the one carrying the task.
func TestStartingAClonePutsTheListOnWatch(t *testing.T) {
b := testBrowser("ubuntu-tpl", "web01")
b.rows[0].task = &runningTask{what: "clone", progress: 2}
if !b.watchLive() {
t.Fatal("watchLive did not report that it had to turn live mode on")
}
if !b.live {
t.Fatal("live mode is off after a clone was started")
}
if d := b.liveIn(); d > liveBusy || d < liveBusy/2 {
t.Errorf("the first refresh is %s away, want about %s", d, liveBusy)
}
// Already on is not a reason to touch it: a tick that was due in half a
// second must not be pushed back to two because a second clone was started.
b.liveNext = time.Now().Add(liveBusy / 4)
due := b.liveNext
if b.watchLive() {
t.Error("watchLive reported turning on a mode that was already on")
}
if !b.liveNext.Equal(due) {
t.Errorf("the pending tick moved from %s to %s", due, b.liveNext)
}
}
+1 -1
View File
@@ -1 +1 @@
1.3.7
1.3.9