375 lines
12 KiB
Go
375 lines
12 KiB
Go
// sort.go — the order the table is in.
|
|
//
|
|
// One key opens a legend on the status line and the next key picks the order, so
|
|
// the list stays on screen while it rearranges itself in front of you. Ordinary
|
|
// letters cannot be used on their own: the list's filter swallows those.
|
|
//
|
|
// Every order has a natural direction, because that is what asking for it means.
|
|
// Sorting by name means a to z; sorting by processor load means the busiest
|
|
// first, and having to reverse it every time would be a nuisance dressed up as
|
|
// consistency. `r` reverses whatever is current.
|
|
package main
|
|
|
|
import (
|
|
"sort"
|
|
"strings"
|
|
)
|
|
|
|
// sortOrder is one way of arranging the table.
|
|
type sortOrder struct {
|
|
key rune // the letter that picks it
|
|
name string // what it is called, in the title and the legend
|
|
natural bool // its own direction: true means largest or busiest first
|
|
cmp func(a, b vmRow) int
|
|
|
|
// legendBreak starts a new line of the legend at this entry. Thirteen
|
|
// orders do not fit across eighty columns, and a legend that ran off the
|
|
// edge would hide the very choices it exists to offer — so it is two lines,
|
|
// broken where the meaning breaks rather than wherever the width runs out.
|
|
legendBreak bool
|
|
}
|
|
|
|
// sortOrders in the order the legend lists them, which is two groups: first
|
|
// what a machine is doing and what it wants doing to it, then what it is made
|
|
// of and where it lives. The legend breaks between the two.
|
|
var sortOrders = []sortOrder{
|
|
{key: 'n', name: "name", cmp: func(a, b vmRow) int { return cmpText(a.name, b.name) }},
|
|
{key: 'p', name: "power", natural: true,
|
|
cmp: func(a, b vmRow) int { return cmpInt(powerRank(a), powerRank(b)) }},
|
|
{key: 'c', name: "cpu load", natural: true,
|
|
cmp: func(a, b vmRow) int { return cmpLoad(vmRow.cpuLoad, a, b) }},
|
|
{key: 'm', name: "memory in use", natural: true,
|
|
cmp: func(a, b vmRow) int { return cmpLoad(vmRow.memLoad, a, b) }},
|
|
// How many rollback points 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 and at the top coming back up.
|
|
//
|
|
// 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
|
|
// with no snapshots has no age, and sorts to the bottom either way round.
|
|
{key: 'o', name: "snapshot age", natural: true,
|
|
cmp: func(a, b vmRow) int { return cmpLoad(vmRow.snapAge, a, b) }},
|
|
// By what is wrong with the machine, worst first: broken above wants-a-look
|
|
// above nothing to report, and within each the machine with the most to
|
|
// answer for first. Sorting the reasons as text would put "alarm" above
|
|
// "disks need consolidating" and mean nothing at all.
|
|
{key: 'w', name: "issues", natural: true,
|
|
cmp: func(a, b vmRow) int { return cmpIssues(a, b) }},
|
|
|
|
{key: 's', name: "memory size", natural: true, legendBreak: true,
|
|
cmp: func(a, b vmRow) int {
|
|
return cmpInt(int(a.vm.Summary.Config.MemorySizeMB), int(b.vm.Summary.Config.MemorySizeMB))
|
|
}},
|
|
{key: 'u', name: "processors", natural: true,
|
|
cmp: func(a, b vmRow) int {
|
|
return cmpInt(int(a.vm.Summary.Config.NumCpu), int(b.vm.Summary.Config.NumCpu))
|
|
}},
|
|
{key: 'v', name: "vcenter", cmp: func(a, b vmRow) int { return cmpText(a.vc.Name, b.vc.Name) }},
|
|
{key: 'h', name: "host", cmp: func(a, b vmRow) int { return cmpText(a.host, b.host) }},
|
|
{key: 'a', name: "address", cmp: func(a, b vmRow) int { return cmpAddress(a, b) }},
|
|
}
|
|
|
|
// sortReverse is the one legend entry that is not an order of its own.
|
|
const sortReverse = 'r'
|
|
|
|
// powerRank puts a running machine above a suspended one above a stopped one, so
|
|
// that "by power" means what an operator means by it.
|
|
func powerRank(r vmRow) int {
|
|
switch r.powerShort() {
|
|
case "on":
|
|
return 3
|
|
case "susp":
|
|
return 2
|
|
case "off":
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func cmpText(a, b string) int { return strings.Compare(strings.ToLower(a), strings.ToLower(b)) }
|
|
|
|
func cmpInt(a, b int) int {
|
|
switch {
|
|
case a < b:
|
|
return -1
|
|
case a > b:
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// A value that is not there is not a small value. Wherever one can be missing —
|
|
// a load figure on a stopped machine, an address on a machine whose guest is not
|
|
// talking — the machine belongs at the *bottom* of the list, and which end of the
|
|
// comparison that is depends on which way the order naturally runs.
|
|
//
|
|
// Load runs downwards by nature (busiest first), so an unknown load has to
|
|
// compare as the smallest. An address runs upwards (a to z), so a missing address
|
|
// has to compare as the largest. Same rule, opposite polarity; the two functions
|
|
// below say so where it can be checked.
|
|
|
|
// cmpLoad orders two machines by a load figure, unknown lowest — which puts it
|
|
// last under the busiest-first direction this order is asked for with.
|
|
func cmpLoad(load func(vmRow) (float64, bool), a, b vmRow) int {
|
|
x, xok := load(a)
|
|
y, yok := load(b)
|
|
switch {
|
|
case !xok && !yok:
|
|
return 0
|
|
case !xok:
|
|
return -1
|
|
case !yok:
|
|
return 1
|
|
case x < y:
|
|
return -1
|
|
case x > y:
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// issueRank is how bad the machine's worst reason is: two for something broken,
|
|
// one for something that wants a look, nought for nothing to report.
|
|
func issueRank(r vmRow) int {
|
|
rank := 0
|
|
for _, i := range r.issueList() {
|
|
if i.bad {
|
|
return 2
|
|
}
|
|
rank = 1
|
|
}
|
|
return rank
|
|
}
|
|
|
|
// cmpIssues orders by that, and within it by how many reasons there are: a
|
|
// machine with a full disk *and* no Tools is worse off than one with only the
|
|
// disk. Nothing to report is nought and sorts where nought belongs, so the
|
|
// order run the other way up is the machines that are fine, by name.
|
|
func cmpIssues(a, b vmRow) int {
|
|
if n := cmpInt(issueRank(a), issueRank(b)); n != 0 {
|
|
return n
|
|
}
|
|
return cmpInt(len(a.issueList()), len(b.issueList()))
|
|
}
|
|
|
|
// cmpAddress orders by address, unknown highest — which puts it last under the
|
|
// a-to-z direction this order is asked for with.
|
|
func cmpAddress(a, b vmRow) int {
|
|
x, y := a.ip(), b.ip()
|
|
switch {
|
|
case x == "-" && y == "-":
|
|
return 0
|
|
case x == "-":
|
|
return 1
|
|
case y == "-":
|
|
return -1
|
|
}
|
|
return cmpText(x, y)
|
|
}
|
|
|
|
// ------------------------------------------------------------------ the sorting
|
|
|
|
func (b *browser) order() sortOrder { return sortOrders[b.sortBy] }
|
|
|
|
// applySort rearranges the rows and rebuilds what is on screen. The selection
|
|
// follows the machine it was on, which refilter already sees to.
|
|
//
|
|
// Machines that compare equal are left in name order, always ascending, whichever
|
|
// way the sort itself runs: a screen full of machines all at 0 % that reshuffles
|
|
// when the direction is flipped would look like the numbers had changed.
|
|
func (b *browser) applySort() {
|
|
sortRows(b.rows, b.sortBy, b.sortDesc)
|
|
b.refilter()
|
|
}
|
|
|
|
// sortRows is the sorting itself, without a browser: `gvm vm -l` orders the same
|
|
// rows the same way.
|
|
func sortRows(rows []vmRow, by int, desc bool) {
|
|
o := sortOrders[by]
|
|
sort.SliceStable(rows, func(i, j int) bool {
|
|
x, y := rows[i], rows[j]
|
|
n := o.cmp(x, y)
|
|
if desc {
|
|
n = -n
|
|
}
|
|
if n != 0 {
|
|
return n < 0
|
|
}
|
|
return cmpText(x.name, y.name) < 0
|
|
})
|
|
}
|
|
|
|
// findOrder resolves what was asked for on the command line — a letter or a name,
|
|
// as the legend spells them — to one of the orders. Empty means the first, which
|
|
// is by name.
|
|
func findOrder(s string) (int, error) {
|
|
if s == "" {
|
|
return 0, nil
|
|
}
|
|
for i, o := range sortOrders {
|
|
if s == string(o.key) || s == o.name || s == shortName(o.name) {
|
|
return i, nil
|
|
}
|
|
}
|
|
names := make([]string, len(sortOrders))
|
|
for i, o := range sortOrders {
|
|
names[i] = shortName(o.name)
|
|
}
|
|
return 0, errf("cannot sort by %q — try one of: %s", s, strings.Join(names, ", "))
|
|
}
|
|
|
|
// sortLabel is the order as the title shows it: which way, and by what.
|
|
func (b *browser) sortLabel() string {
|
|
arrow := "↑"
|
|
if b.sortDesc {
|
|
arrow = "↓"
|
|
}
|
|
return arrow + " " + b.order().name
|
|
}
|
|
|
|
// 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.
|
|
//
|
|
// 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: "
|
|
|
|
entries := make([]string, 0, len(sortOrders)+1)
|
|
for _, o := range sortOrders {
|
|
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:], " "),
|
|
}
|
|
}
|
|
|
|
// shortName is the legend's spelling: the title has room for the whole name, one
|
|
// line shared with the status does not.
|
|
func shortName(name string) string {
|
|
switch name {
|
|
case "power":
|
|
return "pwr" // as the column is headed, and it keeps the legend inside 80
|
|
case "cpu load":
|
|
return "cpu%"
|
|
case "memory in use":
|
|
return "mem%"
|
|
case "memory size":
|
|
return "size"
|
|
case "processors":
|
|
return "cpus"
|
|
case "vcenter":
|
|
return "vc"
|
|
case "address":
|
|
return "ip"
|
|
case "snapshot age":
|
|
return "old"
|
|
case "snapshots":
|
|
return "snaps"
|
|
case "issues":
|
|
return "why" // as the column is headed
|
|
}
|
|
return name
|
|
}
|
|
|
|
// sortPrompt puts the legend up and waits for one key. Anything that is not a
|
|
// choice leaves the order alone: this is the one prompt in the list that is
|
|
// reached by accident, and doing nothing is the right answer to a stray key.
|
|
func (b *browser) sortPrompt() {
|
|
// The colour every question at the foot of the screen has (colPrompt), and
|
|
// no yes/no hint: this is a menu and not a question answerable with y, but
|
|
// 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.
|
|
cols, _ := termSize()
|
|
lines := sortLegend(cols)
|
|
b.prompt = &prompt{text: lines[0], col: colPrompt}
|
|
if len(lines) > 1 {
|
|
b.prompt.more = strings.Join(lines[1:], " ")
|
|
}
|
|
b.render()
|
|
k := b.keys.next()
|
|
b.prompt = nil
|
|
|
|
if k.special != keyRune {
|
|
return
|
|
}
|
|
if k.r == sortReverse {
|
|
b.sortDesc = !b.sortDesc
|
|
b.applySort()
|
|
b.setStatus(colInfo, "sorted "+b.sortLabel())
|
|
return
|
|
}
|
|
for i, o := range sortOrders {
|
|
if o.key == k.r {
|
|
b.sortBy, b.sortDesc = i, o.natural
|
|
b.applySort()
|
|
b.setStatus(colInfo, "sorted "+b.sortLabel())
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// sortedColumn reports whether this column is the one the table is ordered by, so
|
|
// its heading can be lit up. Matching by name rather than by index keeps the two
|
|
// tables — the columns and the orders — free to be listed in different orders.
|
|
func (b *browser) sortedColumn(header string) bool {
|
|
switch b.order().name {
|
|
case "name":
|
|
return header == "NAME"
|
|
case "power":
|
|
return header == "PWR"
|
|
case "cpu load":
|
|
return header == "CPU%"
|
|
case "memory in use":
|
|
return header == "MEM%"
|
|
case "memory size":
|
|
return header == "MEM"
|
|
case "processors":
|
|
return header == "CPU"
|
|
case "vcenter":
|
|
return header == "VC"
|
|
case "host":
|
|
return header == "HOST"
|
|
case "address":
|
|
return header == "IP"
|
|
case "snapshot age", "snapshots":
|
|
return header == "SNAP"
|
|
case "issues":
|
|
return header == "WHY"
|
|
}
|
|
return false
|
|
}
|