返回 CodeWhale
picker.rs
1 //! Gallery: the mode and status-line pickers, rebuilt from kit parts. They
2 //! double as usage examples for [`Picker`] inside a [`Panel`].
3
4 use std::borrow::Cow;
5
6 use ratatui::{
7 buffer::Buffer,
8 layout::Rect,
9 text::{Line, Span},
10 widgets::Widget,
11 };
12
13 use super::{Entry, arrows};
14 use crate::{
15 Depth, KeyHint, KeyHints, Paint, Panel, Picker, PickerItem, PickerMatches, PickerState,
16 PickerTabs, Role, State, StatusMark, Theme, centered, text,
17 };
18
19 /// The engine's `/mode` rows (`AppMode::Agent`, `Plan`, `Operate`; English
20 /// names from `locales/en.json`), with the hints rewritten verbs first.
21 fn mode_items() -> Vec<PickerItem> {
22 vec![
23 PickerItem::new("Work")
24 .key('1')
25 .detail("Works in this session; asks before edits and commands"),
26 PickerItem::new("Plan")
27 .key('2')
28 .detail("Researches without changing files, then proposes a plan"),
29 PickerItem::new("Operate")
30 .key('3')
31 .detail("Runs parallel agents on your goal and checks their work"),
32 ]
33 }
34
35 fn mode_picker(area: Rect, buf: &mut Buffer, theme: &Theme) {
36 let items = mode_items();
37 let hints = KeyHints::new(vec![
38 KeyHint::new(arrows(theme), "move"),
39 KeyHint::new("Enter", "select"),
40 KeyHint::new("Esc", "cancel"),
41 ]);
42 let popup = centered(area, 72, items.len() as u16 + 8, 44, 8);
43 let inner = Panel::new(Depth::Overlay)
44 .title("Mode")
45 .focused(true)
46 .hints(&hints)
47 .draw(popup, buf, theme);
48 Picker::new(&items, PickerState::new(0)).paint(inner, buf, theme);
49 }
50
51 /// The engine's `/statusline` rows (`StatusItem::all()`, labels and hints
52 /// from `crates/tui/src/config.rs`), with the default footer checked.
53 fn status_items() -> Vec<PickerItem> {
54 let row = |label: &'static str, on: bool, detail: &'static str| {
55 PickerItem::new(label).checked(on).detail(detail)
56 };
57 vec![
58 row("Mode", true, "Work, Plan or Operate"),
59 row("Model", true, "The model the next message goes to"),
60 row(
61 "Context window %",
62 true,
63 "Tokens used of the model's context window",
64 ),
65 row("Session cost", true, "Running total for this session"),
66 row(
67 "Account balance",
68 false,
69 "Prepaid credit left with the active provider",
70 ),
71 row(
72 "Prompt cache hit rate",
73 true,
74 "Share of the prompt served from cache",
75 ),
76 row(
77 "Output tokens",
78 true,
79 "Output tokens of the live or last turn",
80 ),
81 row(
82 "Time to first token",
83 true,
84 "Average wait for the first token",
85 ),
86 row("Output rate", true, "Average tokens per second"),
87 row("Workspace", false, "The directory this session writes to"),
88 PickerItem::new("Git branch")
89 .checked(false)
90 .disabled("Not a git repository"),
91 ]
92 }
93
94 fn status_picker(area: Rect, buf: &mut Buffer, theme: &Theme) {
95 let items = status_items();
96 let shown = items.iter().filter(|i| i.checked == Some(true)).count();
97 let hints = KeyHints::new(vec![
98 KeyHint::new("Space", "toggle"),
99 KeyHint::new("a", "all"),
100 KeyHint::new("n", "none"),
101 KeyHint::new("Enter", "save"),
102 KeyHint::new("Esc", "cancel"),
103 ]);
104 let popup = centered(area, 72, 18, 40, 8);
105 let aside = format!("{shown} of {} shown", items.len());
106 let inner = Panel::new(Depth::Overlay)
107 .title("Status line")
108 .aside(aside)
109 .hints(&hints)
110 .draw(popup, buf, theme);
111 Line::from(Span::styled(
112 "Choose what the footer shows.",
113 theme.fg(Role::Muted),
114 ))
115 .render(
116 Rect {
117 height: inner.height.min(1),
118 ..inner
119 },
120 buf,
121 );
122 let list = Rect {
123 y: inner.y.saturating_add(2),
124 height: inner.height.saturating_sub(2),
125 ..inner
126 };
127 let mut state = PickerState::new(9);
128 state.scroll_into_view(items.len(), list.height);
129 Picker::new(&items, state).paint(list, buf, theme);
130 }
131
132 /// Themes, each in a tab, for the fuzzy picker.
133 fn theme_items() -> Vec<PickerItem> {
134 vec![
135 PickerItem::new("Shoreline").detail("Graphite ground, follows the terminal"),
136 PickerItem::new("Shoreline light").detail("Paper ground"),
137 PickerItem::new("Deep sea").detail("Navy ground, blue ombre"),
138 PickerItem::new("Graphite").detail("The exact token grounds"),
139 PickerItem::new("Reef").detail("Warm dark ground"),
140 PickerItem::new("Tidepool").detail("Teal accents on dark"),
141 PickerItem::new("Sandbar").detail("Warm light ground"),
142 PickerItem::new("Lighthouse")
143 .detail("High contrast light")
144 .disabled("Needs a light terminal"),
145 ]
146 }
147
148 fn theme_tabs() -> (Vec<Cow<'static, str>>, Vec<Option<usize>>) {
149 let labels = vec!["All".into(), "Dark".into(), "Light".into()];
150 let of = vec![
151 Some(1),
152 Some(2),
153 Some(1),
154 Some(1),
155 Some(1),
156 Some(1),
157 Some(2),
158 Some(2),
159 ];
160 (labels, of)
161 }
162
163 /// The query "shore" over the themes: matched characters underlined, the
164 /// best match first, the count at the right.
165 fn picker_query(area: Rect, buf: &mut Buffer, theme: &Theme) {
166 let items = theme_items();
167 let query = "shore";
168 let matches = PickerMatches::rank(&items, query, None);
169 Picker::new(&items, PickerState::new(0))
170 .query(query, query.len())
171 .matches(&matches)
172 .paint(area, buf, theme);
173 }
174
175 /// Tabs across the top, and a preview beside the list where there is room
176 /// (it drops below 56 columns).
177 fn picker_tabs_preview(area: Rect, buf: &mut Buffer, theme: &Theme) {
178 let items = theme_items();
179 let (labels, of) = theme_tabs();
180 let tabs = PickerTabs::new(&labels, 1).assign(&of).all(0);
181 let matches = PickerMatches::rank(&items, "", Some(&tabs));
182 let preview = |area: Rect, buf: &mut Buffer, theme: &Theme, item: &PickerItem, _: usize| {
183 let (width, ascii) = (usize::from(area.width), theme.ascii());
184 let label = text::display_safe(&item.label);
185 let detail = text::display_safe(item.detail.as_deref().unwrap_or_default());
186 let mut lines = vec![
187 Line::from(Span::styled(
188 text::truncate(&label, width, ascii).into_owned(),
189 theme
190 .fg(Role::Foreground)
191 .add_modifier(ratatui::style::Modifier::BOLD),
192 )),
193 Line::from(Span::styled(
194 text::truncate_words(&detail, width, ascii).into_owned(),
195 theme.fg(Role::Muted),
196 )),
197 ];
198 for state in [State::Working, State::NeedsYou, State::Done] {
199 lines.push(Line::from(StatusMark::new(state).spans(theme)));
200 }
201 for (i, line) in lines.into_iter().enumerate() {
202 let rect = Rect {
203 y: area.y + i as u16,
204 height: 1,
205 ..area
206 };
207 if rect.y < area.bottom() {
208 line.render(rect, buf);
209 }
210 }
211 };
212 Picker::new(&items, PickerState::new(0))
213 .query("", 0)
214 .tabs(tabs)
215 .matches(&matches)
216 .preview(&preview)
217 .paint(area, buf, theme);
218 }
219
220 /// Nothing matches: the empty state names the query and the next step.
221 fn picker_no_match(area: Rect, buf: &mut Buffer, theme: &Theme) {
222 let items = theme_items();
223 let query = "zebra";
224 let matches = PickerMatches::rank(&items, query, None);
225 Picker::new(&items, PickerState::new(0))
226 .query(query, query.len())
227 .matches(&matches)
228 .paint(area, buf, theme);
229 }
230
231 pub(crate) fn entries() -> Vec<Entry> {
232 vec![
233 Entry {
234 name: "mode-picker",
235 width: 80,
236 height: 13,
237 draw: mode_picker,
238 },
239 Entry {
240 name: "status-picker",
241 width: 80,
242 height: 20,
243 draw: status_picker,
244 },
245 Entry {
246 name: "picker-query",
247 width: 64,
248 height: 6,
249 draw: picker_query,
250 },
251 Entry {
252 name: "picker-tabs-preview",
253 width: 80,
254 height: 8,
255 draw: picker_tabs_preview,
256 },
257 Entry {
258 name: "picker-no-match",
259 width: 64,
260 height: 8,
261 draw: picker_no_match,
262 },
263 ]
264 }
265
265 lines RUST