1#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15pub enum Context {
16 Table,
17 Home,
18 Documentation,
19 Query,
20 Find,
21 GoToColumn,
22 Inspector,
23 Info,
24 ValueCounts,
25 SortFilter,
26 PivotMelt,
27 Chart,
28 Describe,
29 Distribution,
30 DistributionDetail,
31 Correlation,
32 CorrelationDetail,
33 DataQuality,
34 Export,
35 Copy,
36 Views,
37 FormatPicker,
38 Retype,
39 Combine,
40 TablePicker,
41 Sample,
42 Hex,
43}
44
45#[derive(Debug, Clone, Copy)]
47pub struct Screen {
48 pub context: Context,
49 pub title: &'static str,
51 pub reached: &'static str,
53 pub groups: &'static [Group],
55}
56
57#[derive(Debug, Clone, Copy)]
60pub struct Group {
61 pub name: &'static str,
62 pub keys: &'static [Key],
63}
64
65#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub struct Key {
68 pub keys: &'static str,
70 pub label: &'static str,
72 pub line: &'static str,
74 pub more: Option<&'static str>,
77 pub run: Run,
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub enum Run {
83 First,
85 Key(&'static str),
87 Never,
89}
90
91pub const fn k(keys: &'static str, label: &'static str, line: &'static str) -> Key {
93 Key {
94 keys,
95 label,
96 line,
97 more: None,
98 run: Run::First,
99 }
100}
101
102impl Key {
103 pub const fn more(self, more: &'static str) -> Self {
105 Key {
106 more: Some(more),
107 ..self
108 }
109 }
110
111 pub const fn run(self, spec: &'static str) -> Self {
113 Key {
114 run: Run::Key(spec),
115 ..self
116 }
117 }
118
119 pub const fn no_run(self) -> Self {
121 Key {
122 run: Run::Never,
123 ..self
124 }
125 }
126
127 pub fn long(&self) -> &'static str {
129 self.more.unwrap_or(self.line)
130 }
131
132 pub fn action(&self) -> Option<Chord> {
134 match self.run {
135 Run::First => chord(first_key(self.keys)),
136 Run::Key(spec) => chord(spec),
137 Run::Never => None,
138 }
139 }
140}
141
142#[derive(Debug, Clone, Copy, PartialEq, Eq)]
145pub struct Chord {
146 pub code: Code,
147 pub ctrl: bool,
148 pub alt: bool,
149 pub shift: bool,
150}
151
152#[derive(Debug, Clone, Copy, PartialEq, Eq)]
153pub enum Code {
154 Char(char),
155 Enter,
156 Esc,
157 Tab,
158 BackTab,
159 Backspace,
160 Delete,
161 Insert,
162 Up,
163 Down,
164 Left,
165 Right,
166 PageUp,
167 PageDown,
168 Home,
169 End,
170 F(u8),
171}
172
173pub fn chords(keys: &str) -> Vec<Chord> {
176 let spaced = keys.replace(['(', ')'], " / ");
177 spaced
178 .split(" / ")
179 .flat_map(|part| part.split(", "))
180 .flat_map(|part| part.split(" or "))
181 .map(str::trim)
182 .flat_map(|token| {
183 if token.len() > 1 && token.contains('/') {
185 token
186 .split('/')
187 .filter(|t| !t.is_empty())
188 .collect::<Vec<_>>()
189 } else {
190 vec![token]
191 }
192 })
193 .filter_map(chord)
194 .collect()
195}
196
197pub fn first_key(keys: &str) -> &str {
200 let mut end = keys.len();
201 for sep in [" / ", ", ", " (", " or ", " "] {
202 if let Some(at) = keys.find(sep)
203 && at > 0
204 {
205 end = end.min(at);
206 }
207 }
208 let first = &keys[..end];
209 match first.find('/') {
211 Some(at) if at > 0 && first.len() > 1 => &first[..at],
212 _ => first,
213 }
214}
215
216pub fn chord(spec: &str) -> Option<Chord> {
219 let mut chord = Chord {
220 code: Code::Esc,
221 ctrl: false,
222 alt: false,
223 shift: false,
224 };
225 let mut rest = spec;
226 loop {
227 if let Some(r) = rest.strip_prefix("Ctrl+").filter(|r| !r.is_empty()) {
228 chord.ctrl = true;
229 rest = r;
230 } else if let Some(r) = rest.strip_prefix("Alt+").filter(|r| !r.is_empty()) {
231 chord.alt = true;
232 rest = r;
233 } else if let Some(r) = rest.strip_prefix("Shift+").filter(|r| !r.is_empty()) {
234 chord.shift = true;
235 rest = r;
236 } else {
237 break;
238 }
239 }
240 chord.code = match rest {
241 "Enter" => Code::Enter,
242 "Esc" => Code::Esc,
243 "Tab" if chord.shift => Code::BackTab,
244 "Tab" => Code::Tab,
245 "Space" => Code::Char(' '),
246 "Backspace" => Code::Backspace,
247 "Delete" | "Del" => Code::Delete,
248 "Insert" => Code::Insert,
249 "PgUp" | "PageUp" => Code::PageUp,
250 "PgDn" | "PageDown" => Code::PageDown,
251 "Home" => Code::Home,
252 "End" => Code::End,
253 "↑" | "↑↓" => Code::Up,
254 "↓" => Code::Down,
255 "←" | "←→" => Code::Left,
256 "→" => Code::Right,
257 _ => {
258 let mut chars = rest.chars();
259 match (chars.next(), chars.next()) {
260 (Some(c), None) if c.is_ascii_alphabetic() && chord.ctrl => {
263 Code::Char(c.to_ascii_lowercase())
264 }
265 (Some(c), None) if !c.is_whitespace() => {
266 if c.is_ascii_uppercase() {
267 chord.shift = true;
268 }
269 Code::Char(c)
270 }
271 (Some('F'), Some(_)) => Code::F(rest[1..].parse().ok().filter(|n| *n <= 12)?),
272 _ => return None,
273 }
274 }
275 };
276 Some(chord)
277}
278
279pub const GLOBAL: Group = Group {
281 name: "Everywhere",
282 keys: &[
283 k("? / F1", "Help", "This screen's keys (F1 in text fields)")
284 .more("This screen's keys. In a text field ? types, and F1 opens the help")
285 .run("F1"),
286 k("Ctrl+O", "Home", "The home screen; abandons a load")
287 .more("The home screen, on the dataset left, its filter kept and selected (typing replaces it); abandons a load"),
288 k(
289 "Ctrl+Q / Ctrl+C",
290 "Quit",
291 "Quit (Ctrl+C in a text field too)",
292 )
293 .more("Quit from anywhere; Ctrl+C quits from a text field too, where Alt+W copies"),
294 k("Click a field", "Act", "Focus a form's field and act as Space")
295 .more("Focus a form's field and act as Space: a checkbox flips, a choice steps, a picker opens, a button runs; a text field takes the cursor. A row of a list (a sort, a filter, a column) takes focus on the first click and acts on the second. A click on a picker's line chooses it, on a tab switches to it, and on a key in the footer presses it. A click outside a dialog does nothing. The mouse is the terminal's with display.mouse = false (or --mouse false); in most terminals Shift+drag selects text either way")
296 .no_run(),
297 k("Right-click a field", "Back", "Step a choice back, as ← does; else focus it").no_run(),
298 ],
299};
300
301pub const HELP: Group = Group {
303 name: "Help",
304 keys: &[
305 k("↑ / ↓ (j/k)", "Move", "Move between the keys"),
306 k("Enter", "Run", "Close the help and press the key")
307 .more("Close the help and press the key on the line, where help was opened"),
308 k("/", "Filter", "Narrow to the keys whose text matches"),
309 k("Esc", "Close", "Clear the filter, then close"),
310 ],
311};
312
313pub const REFERENCE_URL: &str =
315 "https://derekwisong.github.io/datui/reference/keyboard-shortcuts.html";
316
317pub const Q_SUMMARY: &[(&str, &str)] = &[
320 (
321 "select [cols] [by groups] [where conds]",
322 "Each part optional",
323 ),
324 (
325 "select a, b where a > 10, b < 5",
326 "In where, , is and; | is or",
327 ),
328 (
329 "select avg price, n:count id by region",
330 "Aggregate by group",
331 ),
332 (
333 "total:a+b col[\"first name\"]",
334 "Name a column; a name with spaces",
335 ),
336 ("1/c+a is 1/(c+a)", "Right to left: 100 < (a+b)*2"),
337 (
338 "date.year 2024.01.31 city.contains[\"York\"]",
339 "Accessors and dates",
340 ),
341 (
342 "name in [\"Ann\", \"Bo\"] item like \"*ham*\"",
343 "Membership and patterns",
344 ),
345];
346
347const SAMPLE_KEYS: &[Key] = &[
349 k("s", "Sample", "Choose the sample every tool reads").more(
350 "Choose the sample every tool reads: which rows, how they are picked, how many (typed, like 50000, 50k or 2m), the seed. Enter applies the form",
351 ),
352 k(
353 "v",
354 "Sample rows",
355 "View the sample's rows as a table; Esc comes back",
356 ),
357 k(
358 "r",
359 "Resample",
360 "Draw another sample (when the result is a sample)",
361 ),
362 k("a", "All rows", "Read every row instead, after confirming"),
363 k("t", "New rows", "Read again with rows that arrived since").more(
364 "While following a file, read again with the rows that arrived since the results were read",
365 ),
366];
367
368pub const SCREENS: &[Screen] = &[
370 Screen {
371 context: Context::Table,
372 title: "Table",
373 reached: "Where a dataset opens.",
374 groups: &[
375 Group {
376 name: "Explore",
377 keys: &[
378 k("↑ / ↓ (j/k)", "Move", "Move the row cursor"),
379 k("← / → (h/l)", "Column", "Move the column cursor")
380 .more("Move the column cursor, frozen columns included; the columns scroll only when it would leave the screen"),
381 k("Shift+← / Shift+→", "Page columns", "A page of columns left or right")
382 .more("A page of columns left or right, the cursor on the page's first column"),
383 k("{ / }", "First, last", "First column, last column"),
384 k("PgUp / PgDn", "Page", "A page up or down (Ctrl+B / Ctrl+F too)"),
385 k("Ctrl+D / Ctrl+U", "Half page", "Half a page down or up"),
386 k("Home / End", "Top, end", "First or last row (G = End)"),
387 k(":", "Command line", "Go to a row, or run SQL or q")
388 .more("The command line: digits go to that row (the prefix says row:), anything else runs as SQL or q, as the prefix says; Ctrl+T switches. With a query in effect it opens on the query's text"),
389 k("g", "Go to column", "Go to a column by name"),
390 k("/ (f)", "Find", "Find text, a regex, or letters in order")
391 .more("Find text, a regex, or letters in order in the view; matches on screen light up as you type, and Enter takes the cursor, column cursor and all, to the first at or after its row. f opens it too"),
392 k("n / N", "Next", "Next or previous match")
393 .more("Next / previous match from the cursor's cell, wrapping round the view"),
394 k("Enter", "Drill", "Drill down to a group's rows, or inspect")
395 .more("On a row of a by query or a SQL GROUP BY, drill down to its rows (Esc comes back); elsewhere, inspect the row"),
396 k("Space", "Inspect", "Inspect the row: every field, whole")
397 .more("Inspect the row: every field, each value whole and exact (Esc or Space closes)"),
398 ],
399 },
400 Group {
401 name: "Shape",
402 keys: &[
403 k("[ / ]", "Sort", "Sort by the cursor's column, up or down")
404 .more("Sort by the cursor's column, [ ascending and ] descending, replacing the sort in effect; the same key again on that column removes it. The s sidebar adds secondary sorts"),
405 k("s", "Sort & Filter", "Sort & Filter sidebar, on the cursor's column")
406 .more("Open the Sort & Filter sidebar (tabs: Columns, Filters), on the cursor's column"),
407 k("+ / -", "Filter", "Keep (+) or drop (-) rows with this cell's value")
408 .more("Filter on the cursor's cell: + keeps the rows with its value, - drops them (a null cell: the nulls). Each adds a filter to the Sort & Filter sidebar, joined with \"and\"; the value is the cell's exactly as stored"),
409 k("r", "Reverse", "Reverse the sort, or the row order")
410 .more("Reverse sort order (sorted columns carry a direction mark in the header); with no sort, reverse the row order"),
411 k("H / L", "Move column", "Move the cursor's column left or right")
412 .more("Move the cursor's column one place left or right, the cursor with it; a frozen column moves among the frozen ones. R puts the order back"),
413 k("p", "Pivot & Melt", "Pivot or melt"),
414 k("S", "Sample", "Draw a sample into memory; the view works on it")
415 .more("Draw a sample of the source into memory: the step under the query, filters and sort, which then run over it. The rows show as they arrive (Esc stops, keeping them); the footer says sample 100,000 of 36.8M. Analysis, charts and export read the sample. S again edits it; Method No sample, or R, takes it away"),
416 k("R", "Reset", "Clear query, filters, sort, layout, view")
417 .more("Reset table: clear the sample, query, filters, sort, column order, hidden columns and widths, frozen columns, pivot/melt, drill-down and the applied view"),
418 ],
419 },
420 Group {
421 name: "Analyze",
422 keys: &[
423 k("F", "Counts", "Value counts of the cursor's column")
424 .more("Value counts of the cursor's column: each value's rows, percent and a bar, with a summary"),
425 k("a", "Analysis", "Describe, distributions, correlation, quality")
426 .more("Open Analysis. In a Data Quality evidence drill a is disabled; Esc returns to the observation"),
427 k("c", "Chart", "Chart the view"),
428 k("i", "Info", "Schema, resources, partitions, notes")
429 .more("Open the Info panel (tabs: Schema, Resources, Partitions, Notes). H on its Schema tab reads a CSV's first row as data, or as column names"),
430 ],
431 },
432 Group {
433 name: "Output",
434 keys: &[
435 k("e", "Export", "Export the view to a file"),
436 k("y", "Copy", "Copy a cell, row, view or table")
437 .more("Copy to the clipboard (cell, row, view or table); a cell is the cursor's"),
438 k("v", "Views", "The saved views list"),
439 k("V", "Apply view", "Apply the best-matching view")
440 .more("Apply the best-matching view (or open the list)"),
441 ],
442 },
443 Group {
444 name: "Display",
445 keys: &[
446 k("#", "Row numbers", "Row numbers on or off")
447 .more("Row numbers on or off: each row's place in the source, kept through a sort or a filter (a text file's line numbers). On for text and logs; display.row_numbers sets it"),
448 k(",", "Grouping", "Number formatting (digit grouping) on or off"),
449 k("D", "Types", "The type row under the headers on or off"),
450 k("< / >", "Width", "The cursor's column 4 cells narrower or wider"),
451 k("= / w", "Fit, auto", "Fit the column to the rows on screen; automatic")
452 .more("Fit the cursor's column to the rows on screen; w puts it back to automatic width"),
453 k("b", "Format spec", "Read a binary file with another format spec")
454 .more("A binary file read through a format spec: read it again with another spec. Clears the query, filters and sort"),
455 k("T", "Table", "Open another table of the file")
456 .more("A file of several tables (a workbook's worksheets, a database's tables, a format spec's record types, a Hugging Face cache's splits): pick another to open in place of this one, as --table names it. Clears the query, filters and sort; a saved view for the table applies. On a file of one table, says so"),
457 k("t", "Follow", "Follow the file as it grows")
458 .more("Follow the file as it grows (CSV, TSV, PSV, NDJSON). Reads it again, so the query, filters and sort are cleared; while following, t pauses and resumes, and Esc stops"),
459 ],
460 },
461 Group {
462 name: "Go",
463 keys: &[
464 k("q", "Back", "Home when opened from there, else quit")
465 .more("Back to the home screen when the dataset was opened from it; otherwise quit"),
466 k("Q", "Quit", "Quit"),
467 k("Esc", "Back", "Leave a drill-down; stop a find, sample, follow")
468 .more("Leave a drill-down; stop a find, a sample being drawn (its rows so far stay) or a follow"),
469 ],
470 },
471 Group {
472 name: "Mouse",
473 keys: &[
474 k("Click", "Point", "The cursor to the cell; on a header, its column")
475 .no_run(),
476 k("Double-click", "Enter", "Enter on the row").no_run(),
477 k("Double-click a header", "Sort", "Sort by it: up, down, off, as [ / ] do")
478 .more("Double-click a column's header to sort by it, as [ and ] do: ascending, then descending, then back to no sort, replacing the sort in effect")
479 .no_run(),
480 k("Double-click a header's edge", "Fit", "Fit the column to the rows, as = does")
481 .more("Double-click the gap right of a column's header to fit the column to the rows on screen, as = does; the column cursor moves to it, and nothing is sorted")
482 .no_run(),
483 k("Wheel", "Scroll", "↑ / ↓, three rows a notch")
484 .more("↑ / ↓, three rows a notch; the same in help, the inspector and the sidebars")
485 .no_run(),
486 k("Shift+wheel", "Across", "← / →, the column cursor")
487 .more("← / →, the column cursor (a sideways wheel too)")
488 .no_run(),
489 k("Click a key", "Press", "Press a key the footer shows")
490 .more("Press a key the footer shows; a click on the filters and sort presses s, on query presses :")
491 .no_run(),
492 k("Drag a header", "Move column", "Move the column there, as H / L do")
493 .more("Drag a column's header onto another column to move it there, as H / L do; a rule on the header marks where it lands")
494 .no_run(),
495 k("Drag a header's edge", "Width", "Set the column's width, as < / > do")
496 .more("Drag the gap right of a column's header to set its width by hand, as < / > do, from 4 to 240 cells")
497 .no_run(),
498 k("Right-click", "Menu", "A menu of the keys that act on the cell")
499 .more("Right-click a cell: the cursor goes there and a menu lists the keys that act on it (+, -, F, [, ], y, Space), each with its key. ↑ / ↓ and Enter, or a click, run a line as its key does; Esc or a click elsewhere closes it")
500 .no_run(),
501 ],
502 },
503 ],
504 },
505 Screen {
506 context: Context::Home,
507 title: "Home screen",
508 reached: "`datui` with no path, or <kbd>Ctrl</kbd>+<kbd>O</kbd> from anywhere.",
509 groups: &[
510 Group {
511 name: "Explore",
512 keys: &[
513 k("↑ / ↓", "Move", "Move the selection (Ctrl+P / Ctrl+N too)"),
514 k("Ctrl+↑ / Ctrl+↓", "Section", "Previous or next section"),
515 k("PgUp / PgDn", "Page", "A screenful, stopping at the first and last"),
516 k("Home / End", "First, last", "The first or last row")
517 .more("The first or last row. The filter has no cursor to move: it is edited at its end"),
518 k("← / →", "Fold", "Fold or unfold; → goes inside")
519 .more("Fold or unfold a section; → on a directory or a file of tables goes inside it, and on a file a format spec reads as record types lists them, one row each"),
520 k("Space", "Fold", "Fold the section (types once filtering)")
521 .more("While the filter is empty: fold or unfold the section header under the cursor. With a filter typed, it types"),
522 k("Tab", "Sort", "Cycle the sort")
523 .more("Cycle the sort; the footer names the order in effect when it has room"),
524 ],
525 },
526 Group {
527 name: "Go",
528 keys: &[
529 k("Enter", "Open", "What the footer names: Open, Inside, Look")
530 .more("What the footer says on this row: \"Open all\" reads a whole directory as one table, \"Inside\" steps into it, \"Open\" loads a file, \"Look\" finds out first. A catalog bookmark, indented under its dataset, opens whole. On a section header, fold or unfold it; on the More row, show the rest; on the hidden-files row, show them"),
531 k("Backspace", "Up", "Delete a filter character, or up a level")
532 .more("Delete a filter character; on an empty filter, up a level (from a bucket, back to its cloud source; from the top of a catalog's remote dataset, back here)"),
533 k("Esc", "Back", "Path prompt, filter, directory, then the table")
534 .more("Back out one layer: the path prompt, the filter (onto the first dataset), the directory (back to the row it was entered from), then to the open table"),
535 ],
536 },
537 Group {
538 name: "Find",
539 keys: &[
540 k("(type)", "Filter", "Narrow by name or column, fuzzy")
541 .more("Narrow by name or column; fuzzy, so \"sal\" finds \"sales\". What you open often ranks first. Typing also searches below the directory you are inside and the listed bucket names; matches appear under \"Found\"")
542 .no_run(),
543 k("~", "Path", "Type a path or URL (on an empty filter)")
544 .more("While the filter is empty: type a path or URL by hand. The list shows the directory being typed, narrowed by the name after the last /. The prompt is a plain editor: characters, Backspace, Ctrl+U clears, The first name that matches is picked. Tab completes the one name left or what the names share, or a name picked further down; ↑ / ↓ pick a name, and ↑ from the first takes the path as typed; Enter opens the picked name or the typed path: a file opens, a directory is gone inside; Esc closes. s3://, gs:// and az:// complete from buckets and prefixes already known. With a filter typed, ~ types into it"),
545 k("Ctrl+U", "Clear", "Clear the filter or path input"),
546 k("Ctrl+R", "Refresh", "List again what is on screen"),
547 ],
548 },
549 Group {
550 name: "Manage",
551 keys: &[
552 k("Ctrl+A", "All files", "Show or hide files datui cannot read")
553 .more("Show or hide files datui cannot read; inside a SQLite database, its internal tables"),
554 k("Ctrl+X", "Hex", "The file under the cursor as bytes")
555 .more("Show the local file under the cursor as bytes, in the hex view, whatever datui would read it as"),
556 k("Ctrl+D", "Catalog", "Add the row to catalog.toml; on its rows, forget it")
557 .more("Add the dataset or directory under the cursor to catalog.toml, listed under My datasets; on a row from catalog.toml, forget it. A heading stands for the directory it lists. Only catalog.toml is written; another catalog is hidden with home.hide"),
558 k("Ctrl+E", "Docs", "The row's documentation, full screen")
559 .more("Open the Documentation view of a catalog row, of a place inside one, or of a file whose format spec documents it: description, publisher, license, links, record types, columns with units and value legends, bookmarks. A catalog's description, link and column notes stand over the spec's. Ctrl+E here is not readline's end of line: the filter is edited at its end"),
560 k("Delete", "Forget", "Forget a recent, place or catalog.toml row; hide")
561 .more("Forget the highlighted recent entry, or a whole place after confirming, or a row from catalog.toml, or hide a cloud source. On the Example datasets heading, hide them until datui cache clear, after confirming"),
562 k("Shift+Delete", "Forget all", "Forget every recent entry, after confirming"),
563 ],
564 },
565 Group {
566 name: "Mouse",
567 keys: &[
568 k("Click", "Select", "Select the row; double-click is Enter").no_run(),
569 k("Wheel", "Scroll", "Move the selection three rows")
570 .more("Move the selection three rows, stopping at the ends")
571 .no_run(),
572 ],
573 },
574 ],
575 },
576 Screen {
577 context: Context::Documentation,
578 title: "Documentation",
579 reached: "<kbd>Ctrl</kbd>+<kbd>E</kbd> on the home screen, on a catalog row or a file a format spec documents.",
580 groups: &[Group {
581 name: "Read",
582 keys: &[
583 k("↑ / ↓ (j/k)", "Move", "Move the cursor a line"),
584 k("PgUp / PgDn", "Page", "A page"),
585 k("g / G", "Ends", "The first or last line (Home / End too)"),
586 k("Enter / Space / →", "Values", "Open or close the column's value legend")
587 .more("Open or close the value legend of the column under the cursor: a catalog's values, or a spec's enum"),
588 k("o", "Open", "Open the line's link in the browser, after asking")
589 .more("Open the link on the cursor's line in the system browser. Only http and https links open, and only after a question showing the whole URL (Enter opens, Esc does not). Off over SSH or without a display: y copies the link instead"),
590 k("y", "Copy", "Copy the line's link or value, whole")
591 .more("Copy the link or value on the cursor's line, whole, however it is cut on screen"),
592 k("Esc / q / ←", "Back", "Back to the home screen"),
593 ],
594 }],
595 },
596 Screen {
597 context: Context::Query,
598 title: "Command line",
599 reached: "<kbd>:</kbd> at the table.",
600 groups: &[
601 Group {
602 name: "Run",
603 keys: &[
604 k("(digits)", "Row", "Digits go to that row (row: shows it)")
605 .more("Digits alone go to that row, and the prefix says row: (:0 Enter is the top)")
606 .no_run(),
607 k("Enter", "Run", "Go to the row, or run the query")
608 .more("Go to the row, or run the query as the prefix says, sql: or q:. Reopened with a query in effect, the line holds its text, selected: typing replaces it, arrows edit it"),
609 k("Ctrl+J", "Run", "Run, the same as Enter"),
610 k("Ctrl+T", "Language", "SQL or q; the line keeps its text")
611 .more("Switch between SQL and q, keeping what is typed; the choice is remembered. [query] default_mode sets the first"),
612 k("Esc", "Close", "Close"),
613 ],
614 },
615 Group {
616 name: "Edit",
617 keys: &[
618 k("Tab", "Complete", "Complete a column name (SQL: or df)")
619 .more("Complete a column name, or df in SQL; again for the next match. The line under the input lists the names that fit"),
620 k("Alt+Enter", "New line", "SQL: start a new line"),
621 k("↑ / ↓", "History", "Earlier and later queries (Ctrl+P / Ctrl+N)")
622 .more("Earlier and later queries from the history (Ctrl+P / Ctrl+N too; SQL and q keep their own). In SQL over several lines, they move between lines first"),
623 k("Ctrl+U / Ctrl+K", "Delete", "Delete to the start or end of the line"),
624 k("Ctrl+Z / Ctrl+R", "Undo", "Undo, redo"),
625 ],
626 },
627 ],
628 },
629 Screen {
630 context: Context::Find,
631 title: "Find",
632 reached: "<kbd>/</kbd> (or <kbd>f</kbd>) at the table.",
633 groups: &[
634 Group {
635 name: "Find",
636 keys: &[
637 k("(text)", "Pattern", "Text, a regex, or letters in order")
638 .more("What to find: text (any case until a capital is typed), a regex with Ctrl+R, or letters in order with Ctrl+T. Matches in the rows on screen light up as you type, and the line says how many are on screen")
639 .no_run(),
640 k("Enter", "Next", "Go to the first match at or after the row")
641 .more("Find: the cursor, column cursor and all, goes to the first match at or after its row, reading past the rows on hand when it must (the footer counts the rows read; Esc stops). On an empty field, clear the find"),
642 k("Ctrl+G", "Keep matches", "Keep only the rows that match, as a filter")
643 .more("Keep only the rows with a match, as a filter: the footer and the Sort & Filter sidebar show it, and removing it there (or R) brings the rows back"),
644 k("Ctrl+R", "Regex", "Regex on or off"),
645 k("Ctrl+T", "Letters in order", "Letters in order on or off: smth finds Smith"),
646 k("Ctrl+L", "Column", "Only the cursor's column, or every column")
647 .more("Only the column cursor's column, or every column shown"),
648 k("↑ / ↓", "History", "Earlier patterns (Ctrl+P / Ctrl+N too)"),
649 k("Esc", "Cancel", "Cancel"),
650 ],
651 },
652 Group {
653 name: "At the table",
655 keys: &[
656 k("n / N", "Next", "Next or previous match")
657 .more("Next / previous match from the cursor's cell. Past the last match the find comes round to the first, and the footer says so. Each one typed while a find reads runs in turn; Esc stops them all").no_run(),
658 k("/", "Find again", "Find again, the last pattern ready to edit").no_run(),
659 k("Esc", "Clear", "Clear the find (or stop one still reading)").no_run(),
660 ],
661 },
662 ],
663 },
664 Screen {
665 context: Context::GoToColumn,
666 title: "Go to column",
667 reached: "<kbd>g</kbd> at the table.",
668 groups: &[Group {
669 name: "Go",
670 keys: &[
671 k("(type)", "Narrow", "Narrow to the names that contain it").no_run(),
672 k("↑ / ↓", "Move", "Move"),
673 k("Enter", "Go", "Move the column cursor to it")
674 .more("Go. The column cursor moves to it. A column already whole on screen stays where it is; another becomes the first after the frozen ones, or lands on the last page when it is there"),
675 k("Backspace", "Delete", "Delete a character (Ctrl+W a word, Ctrl+U all)"),
676 k("Esc", "Close", "Close without moving"),
677 ],
678 }],
679 },
680 Screen {
681 context: Context::Inspector,
682 title: "Inspector",
683 reached: "<kbd>Space</kbd> at the table.",
684 groups: &[
685 Group {
686 name: "Fields",
687 keys: &[
688 k("↑ / ↓ (j/k)", "Move", "Move between fields"),
689 k("Home / End", "First, last", "First and last field"),
690 k("PgUp / PgDn", "Page", "A page of fields"),
691 k("← / → (h/l)", "Row", "Previous and next row")
692 .more("Previous and next row; the table's cursor moves with it"),
693 k("Tab / Shift+Tab", "Value", "Into the value, to scroll and find in it")
694 .more("Into the value, to scroll and find in it. Nothing moves: the panes are split by what they hold, not by where the cursor is"),
695 k("Enter", "Open", "Open a struct, list or JSON; read a field")
696 .more("On a group's row, its rows, as at the table. Else open a struct, a list, or text holding a JSON object or array; or read a field the table's rows do not hold (hidden and binary columns)"),
697 k("r", "Read", "On a group's row, read a field the rows lack")
698 .more("On a group's row, read a field the rows do not hold"),
699 k("/", "Find", "Find a field by name, then by value")
700 .more("Find a field by name, then by value: type to narrow, Enter or ↓ keeps the list narrowed, Esc clears it"),
701 k("f", "Nulls", "Nulls shown or hidden (comparing: only diffs)")
702 .more("Nulls: shown or hidden (null and empty fields); comparing, only the fields that differ"),
703 k("s", "Order", "Order: the table's, A-Z, or nulls last"),
704 k("c", "Compare", "Compare with the next row, or the pinned one")
705 .more("Compare: a column for the next row, or the pinned one"),
706 k("m", "Pin", "Pin this row to compare others with; again to unpin"),
707 k("Esc / Space", "Close", "Close; Esc clears a find, then Compare, first")
708 .more("Close. Esc backs out one level at a time: a find first, then Compare, then the inspector"),
709 ],
710 },
711 Group {
712 name: "Output",
713 keys: &[
714 k("y", "Copy", "Copy the value as its view shows it"),
715 k("Y", "Copy row", "Copy the whole row as one JSON object"),
716 k("e", "View", "The value's next view, where it has more than one"),
717 k("w", "Wrap", "Word wrap or hard wrap for long text"),
718 k("o", "Open", "Open the value in another program"),
719 ],
720 },
721 Group {
722 name: "Value",
723 keys: &[
724 k("↑ / ↓ (j/k)", "Scroll", "Scroll a line"),
725 k("PgUp / PgDn", "Page", "Scroll a page"),
726 k("Home / End", "Top, end", "Top and end, however long the value")
727 .more("Top and end, at once however long the value"),
728 k("/", "Find", "Find in the value; n and N go on")
729 .more("Find in the value; n and N go to the next and last place"),
730 k("e, w, y, o", "As fields", "As in the fields"),
731 k("Esc / Tab", "Fields", "Back to the fields; Esc clears a find first")
732 .more("Back to the fields (Shift+Tab too); Esc clears a find first"),
733 ],
734 },
735 Group {
736 name: "Nested",
737 keys: &[
738 k("Enter / → (l)", "Open", "Open the focused item"),
739 k("Tab / Shift+Tab", "Value", "Into the item's value; not in an empty level"),
740 k("Esc / ← (h)", "Up", "Up a level; at the row, Esc closes"),
741 k("y", "Copy", "Copy the item: text, or JSON indented")
742 .more("Copy the focused item: text as itself, a JSON object or array indented"),
743 ],
744 },
745 ],
746 },
747 Screen {
748 context: Context::Info,
749 title: "Info panel",
750 reached: "<kbd>i</kbd> at the table.",
751 groups: &[Group {
752 name: "Panel",
753 keys: &[
754 k("← / → (h/l)", "Tab", "Previous or next tab (Shift+Tab / Tab too)")
755 .more("Previous or next tab, from anywhere in the panel; Shift+Tab and Tab do the same. The panel is a viewer, not a form: its body always has the keys"),
756 k("↑ / ↓ (j/k)", "Move", "Move the cursor, or scroll the list")
757 .more("Schema, Notes or Documentation tab: move the cursor. Model, Audio, MIDI, Metadata and format tabs: scroll the list"),
758 k("PgUp / PgDn", "Page", "Scroll the list a page")
759 .more("Model, Audio, MIDI, Metadata and format tabs: scroll the list a page"),
760 k("Home / End", "Top, end", "The top or the end of the list")
761 .more("Model, Audio, MIDI, Metadata and format tabs: the top or the end of the list"),
762 k("Enter", "Take", "Schema: the type; Notes: the offer; a table: open it")
763 .more("Schema tab: change the column's type, with the names and formats a format spec's type takes. Notes tab: take the offer on the note, where it has one. Documentation tab: open or close the value legend of the column under the cursor. Excel or SQLite tab: open the worksheet or table under the cursor in place of this one, as T does"),
764 k("o", "Open", "Documentation tab: open the line's link")
765 .more("Documentation tab: open the link on the cursor's line in the system browser. Only http and https links open, and only after a question showing the whole URL. Off over SSH or without a display"),
766 k("y", "Copy", "Documentation tab: copy the line's link or value")
767 .more("Documentation tab: copy the link or value on the cursor's line, whole, however it is cut on screen"),
768 k("H", "Header", "Schema tab, CSV: first row as data or names")
769 .more("Schema tab, CSV, TSV, PSV: read the first row as data, under column_1, column_2, …; again to read it as column names. Reads the file again, so the query, filters and sort are cleared, and the panel closes"),
770 k("c", "Count", "Count every row, where the count is an estimate")
771 .more("A dataset of more files than read.exact_count_files shows a row count estimated from a sample of its footers; c reads every footer for the exact count. The footer shows how far it has got, and Esc at the table stops it"),
772 k("x", "Hex", "Show the file's bytes in the hex view"),
773 k("Esc / i", "Close", "Close the panel"),
774 ],
775 }],
776 },
777 Screen {
778 context: Context::ValueCounts,
779 title: "Value counts",
780 reached: "<kbd>F</kbd> at the table.",
781 groups: &[
782 Group {
783 name: "Explore",
784 keys: &[
785 k("↑ / ↓ (j/k)", "Move", "Move"),
786 k("PgUp / PgDn", "Page", "A page"),
787 k("Home / End", "First, last", "First and last row"),
788 k("← / → (h/l)", "Column", "The previous or next column")
789 .more("The previous or next column; the table's column cursor moves with it"),
790 k("Enter", "Drill", "The rows holding the value")
791 .more("The rows holding the value, as a drill-down; Esc there comes back here"),
792 k("s", "Sort", "Sort by count or by value"),
793 k("c", "Histogram", "A number's histogram, or its counts")
794 .more("Between a number column's histogram, binned from the counts, and the listing of its values. A number opens as its histogram"),
795 k("a", "All rows", "Count every row, when the counts are of a sample"),
796 k("t", "New rows", "Count again with the rows that arrived")
797 .more("While following a file, count again with the rows that arrived since; the bar says how many"),
798 ],
799 },
800 Group {
801 name: "Output",
802 keys: &[
803 k("y", "Copy", "Copy the counts as TSV")
804 .more("Copy the counts as TSV: every value, its count, percent and cumulative percent"),
805 k("e", "Export", "Export the counts to a file"),
806 k("Esc", "Back", "Back to the table")
807 .more("Back to the table; while every row is being counted, stop that and keep the sample"),
808 ],
809 },
810 ],
811 },
812 Screen {
813 context: Context::SortFilter,
814 title: "Sort and filter",
815 reached: "<kbd>s</kbd> at the table.",
816 groups: &[
817 Group {
818 name: "Sidebar",
819 keys: &[
820 k("Tab / Shift+Tab (↑ / ↓)", "Next", "Next or previous field")
821 .more("Next or previous field, wrapping; a field the form does not offer right now is skipped. The arrows move from the moment the dialog opens"),
822 k("← / →", "Change", "Tabs on the tab bar; flip a sort; and/or")
823 .more("On the tab bar: switch Sort & Filter / Columns. On a sort: flip its direction. On a filter: and / or. On a column: step its sort (none, ascending, descending). h/l too"),
824 k("Space", "Act", "Flip a sort, edit a filter, add one")
825 .more("On a sort: flip its direction. On a filter: edit it (column, operator, value). On \"add sort\": pick a column to sort by, last and ascending. On \"add filter\": a new filter, starting on the table's column cursor. On a column: step its sort"),
826 k("Enter", "Apply", "Apply and close, from any row")
827 .more("Apply everything staged and close, from any row (in the filter editor Enter takes the step; Ctrl+J applies)"),
828 k("Ctrl+J", "Apply", "Apply from anywhere, mid-edit too")
829 .more("Apply from anywhere, including mid-edit (the row in progress is saved). Ctrl+Enter does the same, on a terminal that tells it from Enter"),
830 k("Esc", "Cancel", "Close a picker or editor, then the sidebar")
831 .more("Close an open picker or filter editor; otherwise cancel and close, discarding what is staged. Reopening shows what is applied"),
832 ],
833 },
834 Group {
835 name: "In effect",
836 keys: &[
837 k("[ / ]", "Move", "Move the sort or filter earlier or later")
838 .more("Move the focused sort earlier or later in the sort order, or the focused filter in the list"),
839 k("d / Del", "Remove", "Remove the sort or filter"),
840 k("C", "Clear", "Remove every sort and filter"),
841 ],
842 },
843 Group {
844 name: "Columns",
845 keys: &[
846 k("(type)", "Narrow", "Narrow the list, with the find field focused")
847 .more("Narrow the column list, when the find field is focused. ↓ from find goes to the list, on the table's column cursor")
848 .no_run(),
849 k("PgUp / PgDn, Home / End", "Page", "A page of columns; the first or last")
850 .more("A page of columns, or the first or last; ↓ stops at the last column"),
851 k("Space", "Sort", "Cycle the sort: none, ascending, descending")
852 .more("Cycle the column's sort: none → ascending → descending (← steps back). Each column carries its own direction"),
853 k("1-9", "Sort place", "Put the column at that place in the sort")
854 .more("Put the column at that place in the sort order; 0 removes it (a digit past the end of the order says so)")
855 .run("1"),
856 k("Del", "Unsort", "Remove the column from the sort"),
857 k("[ / ]", "Sort order", "Earlier or later in the sort order"),
858 k("+ / - (= / _)", "Position", "Move the column's display position"),
859 k("L", "Freeze", "Freeze columns up to this one at the left")
860 .more("Freeze this column and every column up to it at the left edge; on a column already frozen, pull the boundary back"),
861 k("v", "Hide", "Show or hide the column")
862 .more("Show or hide the column (it keeps its place, dimmed)"),
863 k("< / > (, / .)", "Width", "4 cells narrower or wider")
864 .more("Make the column 4 cells narrower or wider. A number column is never narrower than its numbers"),
865 k("f", "Fit", "Fit to the rows on screen")
866 .more("Fit the column to the rows on screen, its name up to the automatic limit"),
867 k("w", "Auto width", "Back to the automatic width"),
868 k("C", "Clear", "Clear staged sort, order, locks, hidden, widths")
869 .more("Clear the staged sort, order, locks, hidden columns and widths"),
870 ],
871 },
872 Group {
873 name: "Filter editor",
874 keys: &[
875 k("(type)", "Narrow", "Narrow the column or operator list")
876 .more("Narrow the column or operator list. The operators are those the column's type takes")
877 .no_run(),
878 k("Enter", "Next", "Choose the step; from the value, save the row")
879 .more("Pick the column (type to narrow, Enter chooses), the operator the same way, then type the value and Enter saves the row. Tab, → and Space also choose at the column and operator steps; Shift+Tab steps back; ↑ / ↓ move in the lists"),
880 k("Esc", "Back", "End the edit, and only the edit"),
881 ],
882 },
883 ],
884 },
885 Screen {
886 context: Context::PivotMelt,
887 title: "Pivot and melt",
888 reached: "<kbd>p</kbd> at the table: the form on the left, a live preview of the result on the right (above and below on a narrow terminal).",
889 groups: &[
890 Group {
891 name: "Form",
892 keys: &[
893 k("Tab / Shift+Tab (↑ / ↓)", "Next", "Next or previous field")
894 .more("Next or previous field, wrapping; a field the form does not offer right now is skipped. The arrows move from the moment the builder opens"),
895 k("← / →", "Change", "Pivot / Melt on the first row; step a choice")
896 .more("On the first row: switch Pivot and Melt. On the aggregation, strategy or type: the previous or next value. On a single column row: the previous or next column. In a text field: move the cursor. h/l too, outside text fields. The preview follows each change"),
897 k("Space", "Open", "Open a column picker; next value of a choice")
898 .more("On a column row: open its picker, scoped to that row; typing narrows it. On a choice: its next value, wrapping"),
899 k("Enter", "Apply", "Apply the reshape the preview shows")
900 .more("Apply the reshape the preview shows, to the whole view, from any field"),
901 k("Esc", "Close", "Close without applying; stop a pivot")
902 .more("Close without applying; while a pivot is computed, stop it and keep the builder"),
903 ],
904 },
905 Group {
906 name: "Picker",
907 keys: &[
908 k("(type)", "Narrow", "Narrow the column list").no_run(),
909 k("↑ / ↓", "Move", "Move; typing narrows"),
910 k("Enter", "Choose", "Choose; on a several-choice row, done"),
911 k("Space", "Toggle", "Choose; toggle a column on a several-choice row"),
912 k("Esc", "Back", "Back out of the picker, undoing its toggles")
913 .more("Back out of the picker; the toggles made since it opened are undone (Enter keeps them)"),
914 ],
915 },
916 ],
917 },
918 Screen {
919 context: Context::Chart,
920 title: "Chart",
921 reached: "<kbd>c</kbd> at the table: a chart chosen from the cursor column's type.",
922 groups: &[
923 Group {
924 name: "Shelves",
925 keys: &[
926 k("1-7", "Type", "Chart type, in order: Line to Heatmap")
927 .more("Switch the chart type directly: Line, Scatter, Bar, Histogram, Box, KDE, Heatmap ([ / ] step). The shelves keep what the new type takes. On Rows, digits type a sample size instead")
928 .run("1"),
929 k("[ / ]", "Type", "Previous or next chart type"),
930 k("Tab / Shift+Tab (↑ / ↓)", "Next", "Next or previous row")
931 .more("Next or previous row of the panel, wrapping (j/k too). A shelf the type does not use is dimmed and skipped"),
932 k("Space / Enter", "Act", "Open a shelf's picker, toggle, or step the row")
933 .more("On X, Y or Color: open its picker. On the line under Color: pick the values that get a series, by rows. On an option: toggle it or take its next value. The panel applies as it changes, so Enter acts as Space does, except on Rows: Space switches between Sample and Every row, and Enter reads what the row says"),
934 k("← / → (h/l)", "Change", "Step the type, bucket, aggregate or option")
935 .more("Step the type, the time bucket (day, week, month, quarter, year), the aggregate (count, distinct, sum, mean, median, stdev, quantile, min, max, first, last) and a quantile's percentile, cumulative, bins, range or order; on Rows, switch between Sample and Every row (read on Enter); on a shelf that takes one column, the previous or next column; on the line under Color, turn Other (every value without a series) on or off; flip a toggle"),
936 k("+ / -", "Adjust", "Bins or bandwidth"),
937 k("0-9", "Size", "On Rows: type a sample size, like 50k or 2m")
938 .more("On Rows: type a sample size, like 50000, 50,000, 50k, 250k or 2m. Backspace edits, Enter reads it, Esc puts the row back as it was. A size of at least the table's rows is Every row. Nothing is read until Enter or focus leaves the row")
939 .no_run(),
940 k("g", "Grid", "Grid on or off")
941 .more("Grid on or off at the labeled ticks: Line, Scatter, Histogram, Box and KDE. [analysis] chart_grid sets where it starts"),
942 k("Esc", "Back", "Back to the table")
943 .more("Back to the table; on Rows with a change waiting, put the row back first"),
944 ],
945 },
946 Group {
947 name: "Plot",
948 keys: &[
949 k("x", "Crosshair", "Line, Scatter: a crosshair reads out the values")
950 .more("Line and Scatter: the plot takes the keys, and a crosshair reads out x and every series' value under the plot. ← / → (h/l) step to the next point or column, Home/End go to the ends; x, Tab or Esc hand the keys back to the panel. A click on the plot puts the crosshair there"),
951 k("e", "Export", "Export the chart to PNG, SVG or PDF")
952 .more("Export the chart to PNG, SVG or PDF, with a title, notes and source. Needs the chart's shelves filled first"),
953 k("t", "New rows", "Draw again with the rows that arrived")
954 .more("While following a file, draw again with the rows that arrived since; the bar says how many"),
955 ],
956 },
957 Group {
958 name: "Picker",
959 keys: &[
960 k("↑ / ↓", "Move", "Move; typing narrows"),
961 k("Enter / Space", "Choose", "Choose (Y, Color values: Space toggles one)")
962 .more("Choose; on a line or scatter chart's Y and on the Color values, Space toggles one in or out (up to 10, fewer on a terminal of fewer colors)"),
963 k("Tab / Shift+Tab", "Next", "Choose and move to the next or previous row"),
964 k("Esc", "Back", "Back out of the picker alone"),
965 ],
966 },
967 Group {
968 name: "Export dialog",
969 keys: &[
970 k("Tab / Shift+Tab (↑ / ↓)", "Next", "Path, format, style, size, legend, words")
971 .more("Next or previous field: Path, Format, Style, Size, Width, Height, Legend, the marks (Opacity and Point size for a scatter, Line width for a line, Y from zero for a line), Title, Description, Notes, Source, Byline, Recipe"),
972 k("← / →", "Change", "Step the format, style, size, legend or marks")
973 .more("Step the format (PNG, SVG, PDF), the style (Light, Dark, Transparent), the size (Slide 16:9, Document, Square, Single column, Double column, Custom), the legend (line ends, a corner, off), a scatter's point opacity (Auto, 100%, 50%, 20%) and size (Small, Medium, Large), a line's width (Thin, Normal, Bold), Y from zero, or the recipe (Include, Omit). Typing a width or height makes the size Custom"),
974 k("Ctrl+P / Ctrl+N", "History", "Earlier or later paths in the path field"),
975 k("Enter", "Export", "Export, from anywhere in the dialog")
976 .more("Export, from anywhere in the dialog. A path ending .png, .svg or .pdf takes that format; any other gets the format's extension after it. An existing file asks Overwrite / No, starting on No; ←/→ (h/l) or Tab pick, Enter confirms, and declining returns to the filled dialog"),
977 k("Esc", "Back", "Back to the chart"),
978 ],
979 },
980 ],
981 },
982 Screen {
983 context: Context::Describe,
984 title: "Analysis: Describe",
985 reached: "<kbd>a</kbd> at the table.",
986 groups: &[
987 Group {
988 name: "Explore",
989 keys: &[
990 k("Tab / Shift+Tab", "Focus", "Between the results and the tools"),
991 k("↑ / ↓ (j/k)", "Move", "Rows, or the sidebar's tools"),
992 k("← / → (h/l)", "Scroll", "Scroll the statistics")
993 .more("Scroll the statistics; the header counts those out of view"),
994 k("Home / End", "First, last", "First or last row"),
995 k("PgUp / PgDn", "Page", "A page"),
996 k("Enter", "Open", "Open the sidebar's tool")
997 .more("Open the sidebar's tool and move into its pane. The first run on a dataset starts in the Sample form, where Enter runs it; later tools reuse that sample. Results last until the view changes: closing and reopening shows them again"),
998 k("Esc", "Back", "Cancel a run; the tools; then close")
999 .more("Cancel a run in progress; otherwise from the results back to the tools, and from the tools close the analysis view"),
1000 ],
1001 },
1002 Group {
1003 name: "Sample",
1004 keys: SAMPLE_KEYS,
1005 },
1006 ],
1007 },
1008 Screen {
1009 context: Context::Distribution,
1010 title: "Analysis: Distribution",
1011 reached: "Distribution in the Analysis sidebar.",
1012 groups: &[
1013 Group {
1014 name: "Explore",
1015 keys: &[
1016 k("↑ / ↓ (j/k)", "Move", "Rows, or the sidebar's tools"),
1017 k("← / → (h/l)", "Scroll", "Scroll the statistics")
1018 .more("Scroll the statistics; the header counts those out of view"),
1019 k("Home / End", "First, last", "First or last row"),
1020 k("PgUp / PgDn", "Page", "A page"),
1021 k("Tab / Shift+Tab", "Focus", "Between the results and the tools"),
1022 k("Enter", "Detail", "Q-Q plot and histogram for the column")
1023 .more("Open detail view for selected column (shows Q-Q plot and histogram); with the sidebar focused, open a tool"),
1024 k("Esc", "Back", "Cancel a run; the tools; then close")
1025 .more("Cancel a run in progress; otherwise from the results back to the tools, and from the tools close the analysis view"),
1026 ],
1027 },
1028 Group {
1029 name: "Sample",
1030 keys: SAMPLE_KEYS,
1031 },
1032 ],
1033 },
1034 Screen {
1035 context: Context::DistributionDetail,
1036 title: "Analysis: Distribution detail",
1037 reached: "<kbd>Enter</kbd> on a column in Distribution.",
1038 groups: &[Group {
1039 name: "Detail",
1040 keys: &[
1041 k("↑ / ↓ (j/k)", "Family", "Compare the values with another family"),
1042 k("Home / End", "First, last", "The first or last family"),
1043 k("s", "Scale", "Histogram scale: linear or log"),
1044 k("Esc", "Back", "Back to the distribution table"),
1045 ],
1046 }],
1047 },
1048 Screen {
1049 context: Context::Correlation,
1050 title: "Analysis: Correlation",
1051 reached: "Correlation Matrix in the Analysis sidebar.",
1052 groups: &[
1053 Group {
1054 name: "Explore",
1055 keys: &[
1056 k("Tab / Shift+Tab", "Focus", "Between the matrix and the tools"),
1057 k("↑ / ↓ (j/k)", "Move", "Matrix rows, or the sidebar's tools"),
1058 k("← / → (h/l)", "Column", "Matrix columns"),
1059 k("Home / End", "Ends", "The first or last pair")
1060 .more("Jump to the first pair (the first row's second column) or the last (the last row's next-to-last column); the matrix opens on the first"),
1061 k("PgUp / PgDn", "Page", "A page"),
1062 k("Enter", "Detail", "The pair's detail (not on the diagonal)")
1063 .more("Open pair detail view (on a cell) or open a tool (sidebar); does nothing on a diagonal cell"),
1064 k("m", "Method", "Pearson or Spearman, named in the title")
1065 .more("Method: Pearson r or Spearman ρ, named in the title (both come from the one run, so it reads nothing)"),
1066 k("Esc", "Back", "Cancel a run; the tools; then close")
1067 .more("Cancel a run in progress; otherwise from the matrix back to the tools, and from the tools close the analysis view"),
1068 ],
1069 },
1070 Group {
1071 name: "Sample",
1072 keys: SAMPLE_KEYS,
1073 },
1074 ],
1075 },
1076 Screen {
1077 context: Context::CorrelationDetail,
1078 title: "Analysis: Correlation detail",
1079 reached: "<kbd>Enter</kbd> on a pair in the correlation matrix.",
1080 groups: &[Group {
1081 name: "Detail",
1082 keys: &[
1083 k("m", "Method", "Pearson or Spearman"),
1084 k("Esc", "Back", "Back to the correlation matrix")
1085 .more("Return to the correlation matrix. Resampling (r) works from the matrix, not from inside this detail"),
1086 ],
1087 }],
1088 },
1089 Screen {
1090 context: Context::DataQuality,
1091 title: "Analysis: Data Quality",
1092 reached: "Data Quality in the Analysis sidebar.",
1093 groups: &[
1094 Group {
1095 name: "Setup",
1096 keys: &[
1097 k("↑ / ↓, Tab", "Move", "Move between rows"),
1098 k("← / →", "Choose", "Change a short list's choice"),
1099 k("Space", "Open", "Open the row's form or list")
1100 .more("Open the row: the Sample form, a list (type to narrow), Time roles, Intervals, Column intent or Expected"),
1101 k("s", "Sample", "The Sample form")
1102 .more("The Sample form; its Enter applies to Setup and returns there"),
1103 k("p", "Plan", "The access plan: what a run reads"),
1104 k("d", "Release", "Release kept rows and the local copy")
1105 .more("Release the rows runs kept and a full scan's local copy, named on the Read rule; the next run that would reuse them reads again"),
1106 k("Enter", "Run", "Run, from any row")
1107 .more("Run, from any row. A full read asks first; the report on screen or in the session cache is shown, not read again"),
1108 k("Esc", "Discard", "Discard every staged edit"),
1109 ],
1110 },
1111 Group {
1112 name: "Setup lists",
1113 keys: &[
1114 k("↑ / ↓", "Move", "Choose the role, pair, column or row")
1115 .more("Time roles: choose the role. Intervals: choose the pair. Column intent: choose the column. Expected: Windows, From, Before (Tab too)"),
1116 k("← / →", "Choose", "Time roles: its column; Expected: windows")
1117 .more("Time roles: choose the role's column. Expected: choose the windows. Intervals: measure the pair or not"),
1118 k("Space", "Toggle", "Intervals: measure or not; intent: its form")
1119 .more("Intervals: measure the pair or not. Column intent: declare the column's intent in a form"),
1120 k("Enter", "Done", "Done"),
1121 k("Esc", "Undo", "Put the list back as it was"),
1122 ],
1123 },
1124 Group {
1125 name: "Report",
1126 keys: &[
1127 k("← / → (h/l)", "Page", "Previous or next page"),
1128 k("1 - 5", "Page", "Overview, Columns, Segments, Trends, Intervals")
1129 .run("1"),
1130 k("↑ / ↓ (j/k)", "Move", "Move, or scroll a tall finding"),
1131 k("PgUp / PgDn", "Page", "A page; Home / End the ends")
1132 .more("Page; Home/End jump to either end"),
1133 k("Enter", "Open", "Open a finding, then its rows")
1134 .more("Open a finding, then its rows. On an empty page, open the setting it needs"),
1135 k("c / t", "Only", "Overview: one column's or type's findings")
1136 .more("Overview: only one column's or one type's findings"),
1137 k("o", "Order", "Overview: ranked, by rows, by rate"),
1138 k("e", "Setup", "Setup"),
1139 k("s", "Sample", "Setup, with the Sample form open"),
1140 k("v", "Sample rows", "View the sample's rows; Esc returns"),
1141 k("p", "Plan", "The access plan: what a run reads"),
1142 k("r", "Resample", "On a sample, run again with a new seed"),
1143 k("x", "Export", "Export the report to JSON or Markdown")
1144 .more("Export the report to JSON or Markdown; nothing is read"),
1145 k("Tab / Shift+Tab", "Focus", "Between the result and the tools"),
1146 k("Esc", "Back", "Back one level: all findings, the tools, close")
1147 .more("Back one level: all findings again, then the tools, then close"),
1148 ],
1149 },
1150 Group {
1151 name: "Segments",
1152 keys: &[
1153 k("Enter", "Compare", "The segment's columns beside its comparison")
1154 .more("A segment's columns beside the one it is compared with, largest change first"),
1155 k("o", "Order", "Largest change first, or back in order"),
1156 k("b", "Base", "Compare with the highlighted segment"),
1157 ],
1158 },
1159 Group {
1160 name: "Trends",
1161 keys: &[
1162 k("Enter", "Bars", "A line's bars, with rate and interval")
1163 .more("A line's bars: span, segments, rows sampled of counted, rate, 95% interval, and the bar before. ↑ / ↓ there: the next bar"),
1164 k("m", "Measure", "Next measure"),
1165 k("w", "Coarser", "Stage a coarser window in Setup")
1166 .more("Stage a coarser window in Setup, for segments the sample reached thinly or not at all"),
1167 k("g", "Gaps", "The expected windows with no rows")
1168 .more("The expected windows with no rows: empty by exact count, not sampled, or out of scope"),
1169 k("Esc", "Back", "Back to Trends"),
1170 ],
1171 },
1172 Group {
1173 name: "Intervals",
1174 keys: &[
1175 k("Enter", "Detail", "An interval's detail; again, its rows")
1176 .more("An interval's detail: its ends, the rows with both, missing and unread ends, negative and zero durations, percentiles and breaches. Enter there shows the rows behind the count under the cursor: a sample's from the rows the run kept"),
1177 k("Esc", "Back", "Back to the list"),
1178 ],
1179 },
1180 ],
1181 },
1182 Screen {
1183 context: Context::Export,
1184 title: "Export",
1185 reached: "<kbd>e</kbd> at the table.",
1186 groups: &[
1187 Group {
1188 name: "Form",
1189 keys: &[
1190 k("Tab / Shift+Tab (↑ / ↓)", "Next", "Next or previous field")
1191 .more("Next or previous field, wrapping; the fields follow the format. The dialog opens on Path, and the arrows move from there"),
1192 k("← / →", "Change", "Step the format or compression; move the cursor")
1193 .more("On Format or Compression: the previous or next value, along the Format row (h/l too). On Header or Source file: toggle. In Path and Delimiter: move the cursor"),
1194 k("Space", "Act", "Toggle a checkbox; next format or compression")
1195 .more("Toggle a checkbox (Header, Source file); on Format or Compression, the next value, wrapping"),
1196 k("Ctrl+P / Ctrl+N", "History", "Earlier or later paths in the path field")
1197 .more("In the path field: the paths exported to before, earlier or later (↑ and ↓ move between fields)"),
1198 k("Enter", "Export", "Export, from anywhere in the form")
1199 .more("Export, from anywhere in the form. On a blank path the form says \"Enter a file path.\" instead of exporting"),
1200 k("Esc", "Close", "Close without exporting"),
1201 ],
1202 },
1203 Group {
1204 name: "File exists",
1205 keys: &[
1206 k("← / → (h/l)", "Pick", "Pick Overwrite or No (Tab too)"),
1207 k("Enter", "Confirm", "Confirm the one picked"),
1208 k("Esc", "No", "Decline: back to the filled form"),
1209 ],
1210 },
1211 ],
1212 },
1213 Screen {
1214 context: Context::Copy,
1215 title: "Copy",
1216 reached: "<kbd>y</kbd> at the table.",
1217 groups: &[
1218 Group {
1219 name: "Form",
1220 keys: &[
1221 k("Tab / Shift+Tab (↑ / ↓)", "Next", "Next or previous row"),
1222 k("← / →", "Change", "Step the scope, column or format")
1223 .more("The previous or next scope, column or format (h/l too); on Header: toggle"),
1224 k("Space", "Act", "Next scope or format; pick a column; toggle")
1225 .more("On Scope or Format: the next value, wrapping. On Column: open its picker. On Header: toggle"),
1226 k("Enter", "Copy", "Copy, from anywhere in the form")
1227 .more("Copy, from anywhere in the form. On the Cell scope with no column picked, Enter re-accents the spec line instead of copying"),
1228 k("Esc", "Close", "Close a picker, then the dialog")
1229 .more("Close a picker, then the dialog, without copying"),
1230 ],
1231 },
1232 Group {
1233 name: "Picker",
1234 keys: &[
1235 k("(type)", "Narrow", "Narrow the list").no_run(),
1236 k("↑ / ↓", "Move", "Move the cursor (j/k narrow)")
1237 .more("Move the cursor (j/k narrow the picker; only ↑/↓ move there)"),
1238 k("Enter / Space", "Choose", "Choose"),
1239 k("Tab / Shift+Tab", "Next", "Choose and move on"),
1240 ],
1241 },
1242 ],
1243 },
1244 Screen {
1245 context: Context::Views,
1246 title: "Views",
1247 reached: "<kbd>v</kbd> at the table.",
1248 groups: &[
1249 Group {
1250 name: "List",
1251 keys: &[
1252 k("↑ / ↓ (j/k)", "Move", "Move in the list"),
1253 k("Enter", "Apply", "Apply the selected view"),
1254 k("s", "Save", "Save the current state as a view")
1255 .more("Save the current state as a view (an untouched table has nothing to save, and s says so)"),
1256 k("e", "Edit", "Edit the selected view"),
1257 k("d", "Delete", "Delete the selected view, after confirming"),
1258 k("i", "Score", "How the view's score was computed")
1259 .more("Show how the selected view's score was computed (Esc closes the score view)"),
1260 k("Esc", "Close", "Close"),
1261 ],
1262 },
1263 Group {
1264 name: "Save and edit",
1265 keys: &[
1266 k("Tab / Shift+Tab (↑ / ↓)", "Next", "Next or previous row")
1267 .more("Next or previous row. In the description ↑ / ↓ move between its lines, and leave it from the first or last"),
1268 k("Enter", "Save", "Save (in the description, Enter types)")
1269 .more("Save, from any row. In the description Enter types: Ctrl+J saves from there"),
1270 k("Ctrl+J", "Save", "Save from anywhere, the description too")
1271 .more("Save from anywhere, the description included; so does Ctrl+Enter, on a terminal that tells it from Enter"),
1272 k("PgUp / PgDn", "Lines", "Five lines in the description"),
1273 k("Space", "Toggle", "Expand Matching; toggle schema match")
1274 .more("Expand or collapse Matching; toggle schema match"),
1275 k("Esc", "Back", "Back to the list, discarding edits"),
1276 ],
1277 },
1278 Group {
1279 name: "Delete",
1280 keys: &[
1281 k("← / → (h/l)", "Pick", "Pick Delete or No (Tab too); it starts on No"),
1282 k("Enter", "Confirm", "Confirm the one picked"),
1283 k("Esc", "No", "Keep the view"),
1284 ],
1285 },
1286 ],
1287 },
1288 Screen {
1289 context: Context::FormatPicker,
1290 title: "Format picker",
1291 reached: "<kbd>b</kbd> at a table read through a format spec.",
1292 groups: &[Group {
1293 name: "Pick",
1294 keys: &[
1295 k("(type)", "Narrow", "Narrow to the names that contain it").no_run(),
1296 k("↑ / ↓", "Move", "Move"),
1297 k("Enter", "Read", "Read the file again with the spec")
1298 .more("Read the file again with the spec chosen. The query, filters and sort are cleared"),
1299 k("Backspace", "Delete", "Delete a character (Ctrl+W a word, Ctrl+U all)"),
1300 k("Esc", "Close", "Close and keep the format"),
1301 ],
1302 }],
1303 },
1304 Screen {
1305 context: Context::Retype,
1306 title: "Column type",
1307 reached: "<kbd>Enter</kbd> on the Info panel's Schema tab, or Change type in the cell menu.",
1308 groups: &[Group {
1309 name: "Pick",
1310 keys: &[
1311 k("(type)", "Narrow", "Narrow the list; a format typed is the format").no_run()
1312 .more("Narrow the list to the names that contain it. In the format list, a strftime format typed that no line holds is the format"),
1313 k("↑ / ↓", "Move", "Move"),
1314 k("Enter", "Choose", "The type, or its format")
1315 .more("The type: the column reads as it at once, a value that does not fit null. A date, time or datetime asks its format next, each line showing what it makes of the column's first value. as read takes the type away"),
1316 k("Backspace", "Delete", "Delete a character (Ctrl+W a word, Ctrl+U all)"),
1317 k("Esc", "Back", "Back to the types, or close"),
1318 ],
1319 }],
1320 },
1321 Screen {
1322 context: Context::Combine,
1323 title: "Combine into datetime",
1324 reached: "Combine into datetime in the cell menu, on a text, date or time column.",
1325 groups: &[Group {
1326 name: "Fields",
1327 keys: &[
1328 k("Tab / Shift+Tab (↑ / ↓)", "Next", "Next or previous field"),
1329 k("Space", "Pick", "Pick a column, or step the kind"),
1330 k("← / →", "Change", "Step the kind: datetime, date or time"),
1331 k("Enter", "Make", "Make the column")
1332 .more("Make the column, before the first column it is made from, as a format spec's derived column is: a date and a time, and a UTC offset, make a datetime in UTC"),
1333 k("Esc", "Cancel", "Cancel"),
1334 ],
1335 }],
1336 },
1337 Screen {
1338 context: Context::TablePicker,
1339 title: "Table picker",
1340 reached: "<kbd>T</kbd> at a table of a file of several.",
1341 groups: &[Group {
1342 name: "Pick",
1343 keys: &[
1344 k("(type)", "Narrow", "Narrow to the names that contain it").no_run(),
1345 k("↑ / ↓", "Move", "Move"),
1346 k("Enter", "Open", "Open the table in place of this one")
1347 .more("Open the table chosen in place of this one. The query, filters and sort are cleared"),
1348 k("Backspace", "Delete", "Delete a character (Ctrl+W a word, Ctrl+U all)"),
1349 k("Esc", "Close", "Close and keep the table"),
1350 ],
1351 }],
1352 },
1353 Screen {
1354 context: Context::Sample,
1355 title: "Sample",
1356 reached: "<kbd>S</kbd> at the table.",
1357 groups: &[Group {
1358 name: "Form",
1359 keys: &[
1360 k("Tab / Shift+Tab (↑ / ↓)", "Next", "Next or previous row"),
1361 k("← / →", "Change", "Step Rows from, Method, Per value of")
1362 .more("Step Rows from, Method and Per value of; Method's No sample takes the view's sample away"),
1363 k("(type)", "Edit", "Type a size (50k, 2m), a seed, rows, values")
1364 .more("Type into the focused row: the size (50000, 50k, 2m), the seed, a row range, partition values or file numbers")
1365 .no_run(),
1366 k("Enter", "Draw", "Draw the sample; again past a memory warning")
1367 .more("Draw the sample: the table shows its rows as they arrive, and the query, filters and sort run over them. When the estimate is more than the memory available now (or analysis.sample_memory_limit), the form says so; Enter again draws anyway"),
1368 k("Esc", "Cancel", "Close; the view's sample stays as it was"),
1369 ],
1370 }],
1371 },
1372 Screen {
1373 context: Context::Hex,
1374 title: "Hex view",
1375 reached: "`datui --hex FILE`, <kbd>Ctrl</kbd>+<kbd>X</kbd> on the home screen, or <kbd>x</kbd> in the Info panel.",
1376 groups: &[
1377 Group {
1378 name: "Explore",
1379 keys: &[
1380 k("← / → (h/l)", "Byte", "A byte"),
1381 k("↑ / ↓ (j/k)", "Row", "A row"),
1382 k("w / b", "Group", "The next group of four, or back one"),
1383 k("0 / $", "Row ends", "The start or end of the row"),
1384 k("g / G", "File ends", "Start or end of the file (Home / End too)")
1385 .more("The start or end of the file (Home and End too)"),
1386 k("PgUp / PgDn", "Page", "A page (Ctrl+B / F; Ctrl+U / D half)")
1387 .more("A page (Ctrl+B/F too; Ctrl+U/D half a page)"),
1388 k(":", "Go to", "Go to an offset: 4096, 0x1000, +16, e-8")
1389 .more("Go to an offset: 4096 or 0x1000; +16 or -16 from the cursor; e-8 for the eighth byte from the end"),
1390 ],
1391 },
1392 Group {
1393 name: "Find",
1394 keys: &[
1395 k("f", "Find", "Find text or bytes (de ad, 0x1acf, ?? any)")
1396 .more("Find bytes. Text is found as its UTF-8 bytes (Ctrl+U in the prompt: as UTF-16 little-endian). 0x1acffc1d, or two or more hex pairs (de ad be ef), is a byte pattern, where ?? matches any byte. Text in double quotes is text, even when it looks like hex. A match may span rows"),
1397 k("n / N", "Next, previous", "Next or previous match, round the end")
1398 .more("The next or previous match, round the end of the file"),
1399 k("R", "Stride", "Bytes per row from the matches' spacing")
1400 .more("When the matches after the one found are all the same distance apart, make that the bytes per row"),
1401 k("Esc", "Stop", "Stop a find that is reading"),
1402 ],
1403 },
1404 Group {
1405 name: "Display",
1406 keys: &[
1407 k("r", "Row size", "Bytes per row; empty: as many as fit")
1408 .more("Bytes per row, so that records line up; empty goes back to as many as fit. --hex-width N sets it on the command line"),
1409 k("#", "Offsets", "Offsets in decimal or hex"),
1410 k("i / Enter", "Inspector", "Show or hide the byte inspector")
1411 .more("Show or hide the byte inspector. Beside the bytes when there is room for it and 16 bytes a row, over them when there is not"),
1412 k("v", "Mark", "Mark a range from the cursor; again to unmark")
1413 .more("Mark from the cursor; move to mark a range, and the status line counts it. v again, or Esc, unmarks"),
1414 ],
1415 },
1416 Group {
1417 name: "Go",
1418 keys: &[
1419 k("B", "Format spec", "Read the file with a format spec instead"),
1420 k("Esc", "Back", "Back to the Info panel or home")
1421 .more("Back to the table, when opened from the Info panel; back home, when opened from there"),
1422 k("q", "Back", "Home, when opened from there; else quit"),
1423 ],
1424 },
1425 ],
1426 },
1427];
1428
1429pub fn screen(context: Context) -> &'static Screen {
1431 SCREENS
1432 .iter()
1433 .find(|s| s.context == context)
1434 .expect("every context has a screen")
1435}
1436
1437pub fn lookup(context: Context, group: Option<&str>, keys: &str) -> Option<&'static Key> {
1440 screen(context)
1441 .groups
1442 .iter()
1443 .filter(|g| group.is_none_or(|name| g.name == name))
1444 .flat_map(|g| g.keys.iter())
1445 .find(|k| k.keys == keys)
1446}
1447
1448pub fn entries() -> impl Iterator<Item = (Option<&'static Screen>, &'static Group, &'static Key)> {
1450 GLOBAL
1451 .keys
1452 .iter()
1453 .map(|key| (None, &GLOBAL, key))
1454 .chain(SCREENS.iter().flat_map(|screen| {
1455 screen
1456 .groups
1457 .iter()
1458 .flat_map(move |group| group.keys.iter().map(move |key| (Some(screen), group, key)))
1459 }))
1460}
1461
1462fn cell(text: &str) -> String {
1464 text.replace('|', "\\|")
1465 .replace('<', "<")
1466 .replace('>', ">")
1467}
1468
1469fn key_cell(label: &str) -> String {
1471 if label.contains('`') {
1472 format!("`` {} ``", label.replace('|', "\\|"))
1473 } else {
1474 format!("`{}`", label.replace('|', "\\|"))
1475 }
1476}
1477
1478fn markdown_table(out: &mut String, keys: &[Key]) {
1479 out.push_str("\n| Key | Action |\n|---|---|\n");
1480 for key in keys {
1481 out.push_str(&format!(
1482 "| {} | {} |\n",
1483 key_cell(key.keys),
1484 cell(key.long())
1485 ));
1486 }
1487}
1488
1489pub fn render_markdown() -> String {
1491 let mut out = String::new();
1492 out.push_str("\n## Every screen\n\nThese keys work on every screen.\n");
1493 markdown_table(&mut out, GLOBAL.keys);
1494 out.push_str("\n## Help\n\n<kbd>?</kbd> or <kbd>F1</kbd>.\n");
1495 markdown_table(&mut out, HELP.keys);
1496 for screen in SCREENS {
1497 out.push_str(&format!("\n## {}\n\n{}\n", screen.title, screen.reached));
1498 for group in screen.groups {
1499 out.push_str(&format!("\n### {} · {}\n", screen.title, group.name));
1500 markdown_table(&mut out, group.keys);
1501 }
1502 }
1503 out
1504}
1505
1506#[cfg(test)]
1507mod tests {
1508 use super::*;
1509
1510 #[test]
1511 fn first_keys_are_read() {
1512 for (keys, first) in [
1513 ("↑ / ↓ (j/k)", "↑"),
1514 ("Ctrl+D / Ctrl+U", "Ctrl+D"),
1515 ("PgUp/PgDn", "PgUp"),
1516 ("/", "/"),
1517 ("+ / - (= / _)", "+"),
1518 ("e, w, y, o", "e"),
1519 ("Enter (d / D)", "Enter"),
1520 ("↑ / ↓, Tab", "↑"),
1521 ("Click a key", "Click"),
1522 ] {
1523 assert_eq!(first_key(keys), first, "{keys}");
1524 }
1525 }
1526
1527 #[test]
1528 fn chords_are_read() {
1529 let c = chord("Ctrl+O").unwrap();
1530 assert!(c.ctrl && !c.shift && c.code == Code::Char('o'));
1531 assert_eq!(chord("G").unwrap().code, Code::Char('G'));
1532 assert!(chord("G").unwrap().shift);
1533 assert_eq!(chord("Shift+Tab").unwrap().code, Code::BackTab);
1534 assert_eq!(chord("F1").unwrap().code, Code::F(1));
1535 assert_eq!(chord("↓").unwrap().code, Code::Down);
1536 assert_eq!(chord("Space").unwrap().code, Code::Char(' '));
1537 assert_eq!(chord("+").unwrap().code, Code::Char('+'));
1538 assert!(chord("Click").is_none());
1539 assert!(chord("(type)").is_none());
1540 assert!(chord("1-9").is_none());
1541 let codes: Vec<Code> = chords("↑ / ↓ (j/k)").iter().map(|c| c.code).collect();
1542 assert_eq!(
1543 codes,
1544 [Code::Up, Code::Down, Code::Char('j'), Code::Char('k')]
1545 );
1546 assert_eq!(chords("PgUp/PgDn").len(), 2);
1547 assert_eq!(chords("/")[0].code, Code::Char('/'));
1548 assert!(chords("(type)").is_empty());
1549 }
1550
1551 #[test]
1554 fn every_entry_is_whole() {
1555 let mut bad = Vec::new();
1556 for (screen, group, key) in entries() {
1557 let at = format!(
1558 "{} · {} · {}",
1559 screen.map_or("Global", |s| s.title),
1560 group.name,
1561 key.keys
1562 );
1563 if key.run != Run::Never && key.action().is_none() {
1564 bad.push(format!("{at}: Enter presses nothing; .run() or .no_run()"));
1565 }
1566 if key.line.contains('\n') || key.line.chars().count() > 52 {
1567 bad.push(format!("{at}: line longer than 52: {}", key.line));
1568 }
1569 if key.label.is_empty() || key.label.split(' ').count() > 3 {
1570 bad.push(format!("{at}: label {:?}", key.label));
1571 }
1572 if key.line.ends_with('.') || key.more.is_some_and(|m| m.ends_with('.')) {
1573 bad.push(format!("{at}: ends with a period"));
1574 }
1575 if key.more == Some(key.line) {
1576 bad.push(format!("{at}: more repeats the line"));
1577 }
1578 }
1579 assert!(bad.is_empty(), "{}", bad.join("\n"));
1580 }
1581
1582 #[test]
1583 fn every_context_has_one_screen() {
1584 use std::collections::HashSet;
1585 let contexts: HashSet<_> = SCREENS.iter().map(|s| s.context).collect();
1586 assert_eq!(contexts.len(), SCREENS.len(), "a context is listed twice");
1587 for screen in SCREENS {
1588 assert!(!screen.groups.is_empty(), "{} has no keys", screen.title);
1589 let mut names = HashSet::new();
1590 for group in screen.groups {
1591 assert!(
1592 names.insert(group.name),
1593 "{}: {} twice",
1594 screen.title,
1595 group.name
1596 );
1597 assert!(!group.keys.is_empty());
1598 }
1599 }
1600 }
1601}