List Views
Most CLI commands that print a collection share the same shape: an optional
intro line, the items, an optional list of messages (warnings, info), and an
optional summary of how many items were filtered out of a larger total. The
list_view builder in standout::views captures that shape once so handlers
stop reassembling it by hand, and #[dispatch(list_view)] wires the result to
the framework's built-in list template without a project-owned template file.
Reach for it whenever a handler returns "here are N things" — a list,
search, or status command — and the result needs more than a bare Vec<T>:
an intro line, a filter summary, or per-item messages.
Where it lives
list_view, ListViewBuilder, and ListViewResult live in
standout::views, a module (pub mod views) rather than a crate-root
re-export:
#![allow(unused)] fn main() { use standout::views::{list_view, ListViewBuilder, ListViewResult}; }
The builder
#![allow(unused)] fn main() { pub fn list_view<T>(items: impl IntoIterator<Item = T>) -> ListViewBuilder<T>; }
ListViewBuilder<T> accepts items in any order and returns itself, so calls
chain:
| Method | Effect |
|---|---|
.intro(text) | A line shown before the items |
.ending(text) | A line shown after the items |
.message(level, text) | Attaches a Message at the given MessageLevel |
.info(text) | Shortcut for .message(MessageLevel::Info, text) |
.success(text) | Shortcut for .message(MessageLevel::Success, text) |
.warning(text) | Shortcut for .message(MessageLevel::Warning, text) |
.error(text) | Shortcut for .message(MessageLevel::Error, text) |
.total_count(n) | Records the unfiltered total, for a "showing X of Y" summary |
.filter_summary(text) | A human-readable description of the active filter |
.tabular_spec(spec) | Attaches a TabularSpec directly (usually left to #[dispatch(list_view, item_type = "...")] instead) |
.build() | Consumes the builder and returns a ListViewResult<T> |
The result
#![allow(unused)] fn main() { pub struct ListViewResult<T> { pub items: Vec<T>, pub intro: Option<String>, pub ending: Option<String>, pub messages: Vec<Message>, pub total_count: Option<usize>, pub filter_summary: Option<String>, pub tabular_spec: Option<TabularSpec>, } }
ListViewResult<T> implements Serialize (fields that are None or empty
are skipped, so --output json stays uncluttered), Default (an empty list
with every optional field unset), and carries .is_empty() and .len()
methods that read items directly.
Because it derives Serialize rather than requiring one, ListViewResult<T>
is itself a valid handler return type: a handler can return
HandlerResult<ListViewResult<Task>> and wrap it in Output::Render like any
other structured output. See
Handler Contract for the
Output enum and the render pipeline it feeds.
Connecting to #[dispatch(list_view)]
The Dispatch derive has a list_view variant attribute that does two
things to a variant's handler:
- It sets the command's template to the framework-provided
standout/list-viewtemplate, unless the variant also sets#[dispatch(template_name = "...")], in which case that name wins. - If
item_type = "..."names a type implementingTabular, the derive wraps the handler so that, on a successfulOutput::Render(list_view_result), it stampslist_view_result.tabular_specwith<ItemType as Tabular>::tabular_spec()before rendering. The handler itself never has to know about the column layout.
Handlers under #[dispatch(list_view)] can be either the two-argument shape
(fn(&ArgMatches, &CommandContext) -> HandlerResult<ListViewResult<T>>) or,
with #[dispatch(simple)] added, a single-argument shape
(fn(&ArgMatches) -> HandlerResult<ListViewResult<T>>). Both are wrapped the
same way.
Worked example
#![allow(unused)] fn main() { use clap::{ArgMatches, Subcommand}; use serde::Serialize; use standout::cli::{CommandContext, Dispatch, HandlerResult, Output}; use standout::views::list_view; use standout::{Tabular, TabularRow}; #[derive(Serialize, Tabular, TabularRow, Clone)] struct Task { #[col(width = 5)] id: u32, #[col(width = 20)] name: String, } mod handlers { use super::*; pub fn list( _matches: &ArgMatches, _ctx: &CommandContext, ) -> HandlerResult<standout::views::ListViewResult<Task>> { let tasks = vec![Task { id: 1, name: "Write docs".to_string(), }]; Ok(Output::Render(list_view(tasks).build())) } } #[derive(Subcommand, Dispatch)] #[dispatch(handlers = handlers)] enum Commands { #[dispatch(list_view, item_type = "Task")] List, } }
Running list renders the framework's list template with Task's column
widths already attached; --output json serializes the same
ListViewResult<Task>, tabular_spec included, with no template involved.
An empty items vec renders the template's "No items found" branch rather
than an empty table.
Disabling the framework template
App::builder().include_framework_templates(false) refuses to build if a
command names standout/list-view (or any other framework template) without
supplying a replacement — a project can opt out of the built-in list layout,
but only by registering its own template under the same name.