1 Commits
Author SHA1 Message Date
Michael Wesemann 4a5477bde7 [mike@mwxm4] 2026-09-08 17:15:05 +02:00
5 changed files with 101 additions and 44 deletions
+14 -12
View File
@@ -178,8 +178,7 @@ be said — `^w`, `--issues` and `snap --old` all name it and date it.
`^o` puts a legend on the status line and the next key picks the order, so the
list stays on screen while it rearranges itself:
sort: n·name p·pwr c·cpu% m·mem% #·snaps o·old w·why
s·size u·cpus v·vc h·host a·ip r·reverse
sort: n·name p·pwr c·cpu% m·mem% z·snaps o·old w·why s·size u·cpus v·vc h·host a·ip r·reverse
Each order comes with its own direction, because that is what asking for it
means: by name is a to z, by processor load is the busiest first. `r` reverses
@@ -194,21 +193,24 @@ direction the order runs. Machines that compare equal stay in name order, so
flipping the direction on a screen full of identical figures does not reshuffle
them.
Two lines, because thirteen choices do not fit across eighty columns and a
legend that ran off the edge would hide the very choices it exists to offer.
They are grouped where the meaning breaks: what the machine is doing and what it
wants doing to it above, what it is made of and where it lives below. The second
line takes the help line's row, which describes keys that do nothing while a
menu is waiting for one.
Thirteen choices are ninety-three columns, so a terminal narrower than that
gets them on two lines instead of one that runs off the right-hand edge, hiding
the very choices the legend exists to offer. They break where the meaning
breaks — what the machine is doing and what it wants doing to it, then what it
is made of and where it lives — and the second line takes the help line's row,
which describes keys that do nothing while a menu is waiting for one. Decided
at render time, so a window dragged wider gets the one line back:
sort: n·name p·pwr c·cpu% m·mem% z·snaps o·old w·why
s·size u·cpus v·vc h·host a·ip r·reverse
Three of them are about the two columns that are new:
* `#` is by how many snapshots the machine is carrying, most first. Nought is a
* `z` is by how many snapshots the machine is carrying, most first. Nought is a
figure here and not a missing one — nothing to clean up is a fact about the
machine — so a machine with none sorts where nought belongs: at the bottom
going down, at the top coming back up. The key is not a letter because every
letter with a mnemonic in it was taken, and a hash is what a count is written
with anyway; `--sort snapshots` needs no shell quoting.
going down, at the top coming back up. The letter carries no mnemonic because
every letter that does was taken; `--sort snaps` spells it out.
* `o` is by the age of the machine's *oldest* snapshot, oldest first, which is
the order the housekeeping is done in — a different question from `#`, and the
more useful one: one snapshot from March wants attention before six from this
+3 -1
View File
@@ -1421,7 +1421,9 @@ func TestEveryQuestionWearsOneColour(t *testing.T) {
lines int // how many of the bottom rows the question occupies
}{
{"the sort legend", func() {
lines := sortLegend()
// Narrower than the whole legend, so it is the two-line one: the
// second line is the part that could quietly lose its colour.
lines := sortLegend(60)
b.prompt = &prompt{text: lines[0], col: colPrompt, more: lines[1]}
b.edit = nil
}, "sort:", 2},
+41 -25
View File
@@ -45,10 +45,10 @@ var sortOrders = []sortOrder{
// the machine — so a machine with none sorts where nought belongs, at the
// bottom going down and at the top coming back up.
//
// The key is not a letter because every letter with a mnemonic in it is
// taken, and a hash is what a count is written with anyway. `--sort
// snapshots` on the command line needs no shell quoting.
{key: '#', name: "snapshots", natural: true,
// The key carries no mnemonic — every letter that does was taken — so it is
// simply one that is free and easy to reach. The name is what the command
// line takes: `--sort snapshots`, or `--sort snaps`.
{key: 'z', name: "snapshots", natural: true,
cmp: func(a, b vmRow) int { return cmpInt(a.snapCount(), b.snapCount()) }},
// By how long the machine has been dragging its oldest snapshot along, the
// oldest first — which is the order the housekeeping is done in. A machine
@@ -233,32 +233,47 @@ func (b *browser) sortLabel() string {
return arrow + " " + b.order().name
}
// sortLegend is the choices, on the two lines they are grouped into: what the
// sortLegend is the choices, laid out for a terminal of this width: one line
// where they fit on one, and otherwise the two groups they fall into — what the
// machine is doing and wants doing to it, then what it is made of and where it
// lives. Terse by necessity — it shares the bottom of the screen with nothing
// but itself — and the title says what the order is anyway, so nobody who
// misses it is lost.
// lives.
//
// The second line is indented under the first one's entries rather than under
// its label, so the two read as one list and not as a sentence continued.
func sortLegend() []string {
// One line is the better answer and the usual one; two is what a narrow
// terminal gets instead of a legend that runs off the right-hand edge, hiding
// the very choices it exists to offer. Decided here, at render time, so a
// window that is dragged wider gets the one line back — the same way the table
// itself is fitted (fitColumns) and the sheet is wrapped.
//
// Terse either way: it shares the bottom of the screen with nothing but itself,
// and the title says what the order is anyway, so nobody who misses it is lost.
func sortLegend(cols int) []string {
const label = "sort: "
lines := []string{label}
entries := make([]string, 0, len(sortOrders)+1)
for _, o := range sortOrders {
entry := string(o.key) + "·" + shortName(o.name)
if o.legendBreak && len(lines) == 1 {
lines = append(lines, SR(" ", len(label)))
}
at := len(lines) - 1
if !strings.HasSuffix(lines[at], " ") {
lines[at] += " "
}
lines[at] += entry
entries = append(entries, string(o.key)+"·"+shortName(o.name))
}
// Reverse is not an order of its own and goes at the end.
entries = append(entries, string(sortReverse)+"·reverse")
if one := label + strings.Join(entries, " "); len([]rune(one)) <= cols {
return []string{one}
}
// Two, broken where the meaning breaks. The second line is indented under
// the first one's entries rather than under its label, so the two read as
// one list and not as a sentence continued.
at := len(sortOrders)
for i, o := range sortOrders {
if o.legendBreak {
at = i
break
}
}
return []string{
label + strings.Join(entries[:at], " "),
SR(" ", len(label)) + strings.Join(entries[at:], " "),
}
// Reverse is not an order of its own and goes at the end of the last line.
lines[len(lines)-1] += " " + string(sortReverse) + "·reverse"
return lines
}
// shortName is the legend's spelling: the title has room for the whole name, one
@@ -298,7 +313,8 @@ func (b *browser) sortPrompt() {
// it is still gvm waiting for a key, and that is one thing wearing one
// colour. Where it takes two lines the second goes in place of the help
// line, which says nothing that applies while a menu is up.
lines := sortLegend()
cols, _ := termSize()
lines := sortLegend(cols)
b.prompt = &prompt{text: lines[0], col: colPrompt}
if len(lines) > 1 {
b.prompt.more = strings.Join(lines[1:], " ")
+42 -5
View File
@@ -221,7 +221,7 @@ func TestSortIsVisibleInTheTable(t *testing.T) {
// Every order is offered, and the legend fits a terminal of eighty.
func TestSortLegend(t *testing.T) {
lines := sortLegend()
lines := sortLegend(80)
legend := strings.Join(lines, "\n")
for _, o := range sortOrders {
if !strings.Contains(legend, string(o.key)+"·"+shortName(o.name)) {
@@ -247,6 +247,35 @@ func TestSortLegend(t *testing.T) {
t.Errorf("the legend wants %d lines, and there is room for two:\n%s", len(lines), legend)
}
// One line wherever one line will do — which is every terminal wide enough
// for it, and the usual case. A legend on two lines is what a narrow
// terminal gets instead of one that runs off the edge.
wide := sortLegend(200)
if len(wide) != 1 {
t.Errorf("a wide terminal gets the legend on %d lines:\n%s", len(wide), strings.Join(wide, "\n"))
}
if n := len([]rune(wide[0])); n > 200 {
t.Errorf("the one-line legend is %d columns wide", n)
}
// And every choice is on it, so nothing is reachable only when the terminal
// happens to be narrow.
for _, o := range sortOrders {
if !strings.Contains(wide[0], string(o.key)+"·"+shortName(o.name)) {
t.Errorf("the one-line legend does not offer %q for %s: %s", string(o.key), o.name, wide[0])
}
}
if !strings.Contains(wide[0], string(sortReverse)+"·reverse") {
t.Errorf("the one-line legend does not offer the reverse: %s", wide[0])
}
// The width at which it gives up on one line is the width of the legend
// itself, and not a number written down somewhere.
if got := sortLegend(len([]rune(wide[0]))); len(got) != 1 {
t.Error("the legend broke in two at exactly its own width")
}
if got := sortLegend(len([]rune(wide[0])) - 1); len(got) != 2 {
t.Error("the legend stayed on one line one column too narrow for it")
}
// Distinct letters, or one of them would be unreachable.
seen := map[rune]bool{sortReverse: true}
for _, o := range sortOrders {
@@ -314,10 +343,10 @@ func TestSortBySnapshotCount(t *testing.T) {
rows[1].snaps = []snapEntry{aged("a", 1), aged("b", 2), aged("c", 3)}
rows[2].snaps = []snapEntry{aged("a", 1)}
if got := orderOf(t, rows, '#', true); got != "three one none" {
if got := orderOf(t, rows, 'z', true); got != "three one none" {
t.Errorf("most snapshots first gave %q", got)
}
if got := orderOf(t, rows, '#', false); got != "none one three" {
if got := orderOf(t, rows, 'z', false); got != "none one three" {
t.Errorf("fewest first gave %q", got)
}
}
@@ -356,14 +385,22 @@ func TestSortByIssues(t *testing.T) {
}
// orderOf sorts the rows given by one order and returns the names in order.
//
// A key that is not an order at all is fatal here rather than left to sort by
// name: the browser's default order is index nought, so a test naming a letter
// that has been renamed would go on passing while checking the name order.
func orderOf(t *testing.T, rows []vmRow, key rune, desc bool) string {
t.Helper()
b := &browser{rows: append([]vmRow(nil), rows...), sortDesc: desc}
found := false
for i, o := range sortOrders {
if o.key == key {
b.sortBy = i
b.sortBy, found = i, true
}
}
if !found {
t.Fatalf("%q is not one of the sort orders", string(key))
}
b.applySort()
var names []string
@@ -384,7 +421,7 @@ func TestTheTwoSnapshotOrdersAskDifferentThings(t *testing.T) {
rows[0].snaps = []snapEntry{aged("a", 1), aged("b", 1), aged("c", 1), aged("d", 1)}
rows[1].snaps = []snapEntry{aged("march", 200)}
if got := orderOf(t, rows, '#', true); got != "many-new one-ancient" {
if got := orderOf(t, rows, 'z', true); got != "many-new one-ancient" {
t.Errorf("by count: %q", got)
}
if got := orderOf(t, rows, 'o', true); got != "one-ancient many-new" {
+1 -1
View File
@@ -1 +1 @@
1.1.2
1.1.3