Approval queues and jumping to anything¶
Two components for scripts and tools that act on many things at once (rs-rich-interact, 0.0.19 workstream 20):
- an approval queue (#471), to review pending items one by one or in bulk and approve, reject or skip each, with a reason;
- a jump navigator (#484), one fuzzy box over several sources (files, headings, symbols, commands, recent places) with the results grouped by source.
Both are built on the same pieces as Select: the item model
with its previews, the fuzzy matcher,
the kit's ListState and TextBuffer, and a keymap. Each
runs under run and the event loop, is tested headless, and degrades to a
line prompt without a terminal. rich approve and rich jump run them
from the shell: see Review and jump.
The approval queue¶
ApprovalQueue::new(prompt, items) lists the items, each with a mark for
its decision: · pending, ✓ approved, ✗ rejected and ↷ skipped. A
pane beside the list (below it on a narrow terminal) shows the focused
item: its label, description, details (Item::meta), its decision and
reason, then its Preview. Enter submits an ApprovalReport: one
Decision for every item, in order, with its value, verdict and
reason. Pending items are in the report too, so a review can stop
half-way and pick up later (.decided(index, verdict, reason)).
The queue only records decisions. Carrying them out (deleting what was approved, sending the rejections back) is the caller's.
| Keys | What they do |
|---|---|
a, r, s |
Approve, reject or skip, then move on to the next pending item |
A, R, S |
The same, asking for a reason first (Enter confirms it, Esc backs out) |
u |
Back to pending |
| Space | Mark the item; a decision then applies to every marked item |
| Ctrl+A | Mark every pending item (again: unmark them) |
n, Tab |
The next pending item |
↑ ↓ (or k j), PgUp PgDn, Home End (or g G) |
Move |
p |
Show or hide the detail pane |
| Enter, Esc | Submit; cancel |
.require_reason(Verdict::Rejected) asks for a reason on every rejection,
whichever key made it, and refuses an empty one. .auto_submit(true)
submits as soon as nothing is pending. .advance(false) keeps the cursor
where it is after a decision. The keys are in context approve and can be
rebound (.rebind("approve", keys("y"))).
With the mouse on (.with_mouse(true)), a click focuses a row, a click on
the cell after the pointer marks it, the wheel moves, and the buttons under
the list ([a] approve, [r] reject, [s] skip, [u] undo, [enter]
submit) are buttons.
use rich_interact::components::{ApprovalQueue, Verdict};
use rich_interact::{run, Item, Outcome, Preview, RunOptions};
let items = requests.iter().map(|request| {
Item::new(request.id, &request.title)
.description(&request.author)
.meta("size", request.size.to_string())
.preview(Preview::Text(request.diff.clone()))
});
let queue = ApprovalQueue::new("Requests", items).require_reason(Verdict::Rejected);
if let Outcome::Done(report) = run(queue, &RunOptions::default())? {
for decision in report.with(Verdict::Rejected) {
reply(decision.value, decision.reason.as_deref().unwrap_or(""));
}
eprintln!("{}", report.summary()); // 3 approved · 1 rejected · 0 skipped · 2 pending
}
Theme keys¶
Decisions are drawn in the console theme's styles, so a theme restyles them without code:
| Theme key | Default |
|---|---|
approve.approved |
bold green |
approve.rejected |
bold red |
approve.skipped |
yellow |
approve.pending |
dim |
The marks read without colour as well. The rest (the pointer, hints, the
answer) comes from the component's Theme, as for
every built-in.
Without a terminal¶
It asks item by item, showing the label, description and details:
a, r or s (or yes, no, skip), with a reason after a colon
(r: not this week). An empty line skips, q stops, and the end of input
leaves the rest pending; a required reason is asked for again. Asked for
defaults, it answers with the report as it stands: nothing decided.
The jump navigator¶
A JumpIndex is a searchable index over JumpSources, each an id, a title
and Items, with an optional prefix character and a limit.
JumpIndex::search(query, only) ranks every source's items and returns
JumpGroups, best first within each, so the index serves a palette of your
own or a test without the component. Jump is the component over it:
use rich_interact::components::{Jump, JumpSource};
use rich_interact::{run, Item, Outcome, Preview, RunOptions};
let jump = Jump::new("Jump")
.source(JumpSource::new("recent", "Recent").items(recent))
.source(JumpSource::new("files", "Files").items(files))
.source(JumpSource::new("headings", "Headings").prefix('#').items(headings))
.source(JumpSource::new("commands", "Commands").prefix('>').items(commands));
if let Outcome::Done(jumped) = run(jump, &RunOptions::default())? {
open(&jumped.source, jumped.value);
}
Typing searches every source at once. Results are grouped under their
source's title, in the order the sources were added, and the best match
overall is focused. Until the search is narrowed, a group shows its best
few (JumpSource::limit, 5 by default) and says how many more matched.
| Keys | What they do |
|---|---|
| Typing, Backspace, Ctrl+U | Search; delete; clear |
A source's prefix first (>, #, @) |
Search that source only |
| Tab, Shift+Tab | Narrow to the next source, or back towards every source |
| ↑ ↓ (Ctrl+P Ctrl+N), PgUp PgDn, Home End | Move across the groups |
| Alt+↑ ↓ | The previous or next group |
| Enter, Esc | Jump; cancel |
Enter returns a Jumped: the source's id, the item's index in it, its
label and its value. The focused item's Preview shows beside the list
(below it when narrow). With the mouse on, a click focuses a result and a
second click picks it, a click on a group's title narrows to that source,
and the wheel moves. The keys are in context jump.
Group titles use the theme keys jump.group (default bold) and
jump.more (dim).
Without a terminal it reads one line and answers with its best match.
Asked for defaults, it answers with the best match for the query it started
with (.query(..)), and has none when nothing was typed.
Testing them¶
Both run in the headless driver:
use rich_interact::components::{ApprovalQueue, Verdict};
use rich_interact::{headless, Outcome};
let queue = ApprovalQueue::new("Deploys", ["api", "web", "db"]);
let script = headless::Script::new().keys("a R").text("flaky").keys("enter s enter");
let (outcome, record) = headless::run(queue, script, 80, 12);
let Outcome::Done(report) = outcome.unwrap() else { panic!() };
assert_eq!(report.verdicts(), [Verdict::Approved, Verdict::Rejected, Verdict::Skipped]);
assert!(record.frames[0].contains("0/3 decided"));
The Python bindings are rs_rich.interact.ApprovalQueue and Jump: see
the Python guide.