| 1 | //! Gallery entries for package Lists: the list in its states and the empty |
| 2 | //! state. The fuzzy picker's entries live in `picker.rs`, beside the other |
| 3 | //! pickers. |
| 4 | |
| 5 | use ratatui::{buffer::Buffer, layout::Rect}; |
| 6 | |
| 7 | use super::Entry; |
| 8 | use crate::{ |
| 9 | EmptyMark, EmptyState, List, ListRow, ListRowState, ListState, Paint, Role, Theme, text, |
| 10 | }; |
| 11 | |
| 12 | /// A settings-style row: a name, its value, and optionally a second line. |
| 13 | /// It shows how a caller paints its own row: `ListRowState::ink` for the |
| 14 | /// name, `Muted` for the rest, a reason in place of the value when disabled. |
| 15 | struct Setting { |
| 16 | name: &'static str, |
| 17 | value: &'static str, |
| 18 | note: Option<&'static str>, |
| 19 | disabled: Option<&'static str>, |
| 20 | } |
| 21 | |
| 22 | impl Setting { |
| 23 | const fn new(name: &'static str, value: &'static str) -> Self { |
| 24 | Self { |
| 25 | name, |
| 26 | value, |
| 27 | note: None, |
| 28 | disabled: None, |
| 29 | } |
| 30 | } |
| 31 | |
| 32 | const fn note(mut self, note: &'static str) -> Self { |
| 33 | self.note = Some(note); |
| 34 | self |
| 35 | } |
| 36 | |
| 37 | const fn disabled(mut self, reason: &'static str) -> Self { |
| 38 | self.disabled = Some(reason); |
| 39 | self |
| 40 | } |
| 41 | } |
| 42 | |
| 43 | impl ListRow for Setting { |
| 44 | fn height(&self, _width: u16) -> u16 { |
| 45 | 1 + u16::from(self.note.is_some()) |
| 46 | } |
| 47 | |
| 48 | fn is_disabled(&self) -> bool { |
| 49 | self.disabled.is_some() |
| 50 | } |
| 51 | |
| 52 | fn paint_row(&self, area: Rect, buf: &mut Buffer, theme: &Theme, state: ListRowState) { |
| 53 | let ascii = theme.ascii(); |
| 54 | let width = usize::from(area.width); |
| 55 | let name_w = (width / 2).min(20); |
| 56 | let name = text::display_safe(self.name); |
| 57 | let name = text::truncate(&name, name_w, ascii); |
| 58 | buf.set_stringn(area.x, area.y, &name, name_w, state.ink(theme)); |
| 59 | let value = self.disabled.unwrap_or(self.value); |
| 60 | let value_w = width.saturating_sub(name_w + 2); |
| 61 | if value_w >= 4 { |
| 62 | let value = text::display_safe(value); |
| 63 | let value = text::truncate(&value, value_w, ascii); |
| 64 | let x = area.x + u16::try_from(name_w + 2).unwrap_or(0); |
| 65 | buf.set_stringn(x, area.y, &value, value_w, theme.fg(Role::Muted)); |
| 66 | } |
| 67 | if let Some(note) = self.note |
| 68 | && area.height > 1 |
| 69 | { |
| 70 | let note = text::display_safe(note); |
| 71 | let note = text::truncate_words(¬e, width, ascii); |
| 72 | buf.set_stringn(area.x, area.y + 1, ¬e, width, theme.fg(Role::Muted)); |
| 73 | } |
| 74 | } |
| 75 | } |
| 76 | |
| 77 | fn settings() -> Vec<Setting> { |
| 78 | vec![ |
| 79 | Setting::new("Theme", "Shoreline, follows terminal"), |
| 80 | Setting::new("Motion", "Full"), |
| 81 | Setting::new("Density", "Comfortable"), |
| 82 | Setting::new("Provider", "Not connected").disabled("Sign in first"), |
| 83 | Setting::new("Language", "English"), |
| 84 | ] |
| 85 | } |
| 86 | |
| 87 | fn list(area: Rect, buf: &mut Buffer, theme: &Theme) { |
| 88 | let rows = settings(); |
| 89 | List::new(&rows, ListState::new(1)).paint(area, buf, theme); |
| 90 | } |
| 91 | |
| 92 | /// Twelve rows in five: the scrollbar, and a selection held in view. |
| 93 | fn list_scrolling(area: Rect, buf: &mut Buffer, theme: &Theme) { |
| 94 | let rows: Vec<String> = (1..=12).map(|n| format!("Session {n:02}")).collect(); |
| 95 | let mut state = ListState::new(8); |
| 96 | state.scroll_into_view(rows.len(), area.height, |_| 1); |
| 97 | List::new(&rows, state).paint(area, buf, theme); |
| 98 | } |
| 99 | |
| 100 | /// Rows two lines tall; the selection takes both. |
| 101 | fn list_tall(area: Rect, buf: &mut Buffer, theme: &Theme) { |
| 102 | let rows = [ |
| 103 | Setting::new("Approvals", "Ask").note("When the agent may run a command without you"), |
| 104 | Setting::new("Sandbox", "Workspace").note("Where files can be written"), |
| 105 | Setting::new("Network", "Off").note("Whether tools can reach the internet"), |
| 106 | Setting::new("Telemetry", "Off").note("What leaves this machine"), |
| 107 | ]; |
| 108 | List::new(&rows, ListState::new(1)).paint(area, buf, theme); |
| 109 | } |
| 110 | |
| 111 | /// Long names are cut with an ellipsis, between graphemes. (CJK text is |
| 112 | /// covered in `tests/lists.rs`: the gallery's own rule check holds the ASCII |
| 113 | /// profile to ASCII, which caller text in another script cannot meet.) |
| 114 | fn list_long(area: Rect, buf: &mut Buffer, theme: &Theme) { |
| 115 | let rows = [ |
| 116 | "Refactor the session store so it keeps every receipt", |
| 117 | "src/components/receipt_table/cell_alignment.rs", |
| 118 | "Untitled", |
| 119 | "A name with trailing words that run past the edge", |
| 120 | ]; |
| 121 | List::new(&rows, ListState::new(1)).paint(area, buf, theme); |
| 122 | } |
| 123 | |
| 124 | fn empty_list(area: Rect, buf: &mut Buffer, theme: &Theme) { |
| 125 | let rows: [&str; 0] = []; |
| 126 | List::new(&rows, ListState::default()) |
| 127 | .empty( |
| 128 | EmptyState::new("No saved sessions") |
| 129 | .action("Start one with a message") |
| 130 | .mark(EmptyMark::Glyph), |
| 131 | ) |
| 132 | .paint(area, buf, theme); |
| 133 | } |
| 134 | |
| 135 | fn empty_state(area: Rect, buf: &mut Buffer, theme: &Theme) { |
| 136 | EmptyState::new("No workflow runs yet") |
| 137 | .body("Runs appear here once a workflow starts, with what each one changed.") |
| 138 | .action("Start one with /workflow, or press n") |
| 139 | .paint(area, buf, theme); |
| 140 | } |
| 141 | |
| 142 | fn empty_state_small(area: Rect, buf: &mut Buffer, theme: &Theme) { |
| 143 | EmptyState::new("Nothing queued") |
| 144 | .body("Messages you send while the agent works wait here.") |
| 145 | .action("Type a message") |
| 146 | .paint(area, buf, theme); |
| 147 | } |
| 148 | |
| 149 | pub(crate) fn entries() -> Vec<Entry> { |
| 150 | vec![ |
| 151 | Entry { |
| 152 | name: "list", |
| 153 | width: 64, |
| 154 | height: 5, |
| 155 | draw: list, |
| 156 | }, |
| 157 | Entry { |
| 158 | name: "list-scrolling", |
| 159 | width: 64, |
| 160 | height: 5, |
| 161 | draw: list_scrolling, |
| 162 | }, |
| 163 | Entry { |
| 164 | name: "list-tall-rows", |
| 165 | width: 64, |
| 166 | height: 6, |
| 167 | draw: list_tall, |
| 168 | }, |
| 169 | Entry { |
| 170 | name: "list-long", |
| 171 | width: 40, |
| 172 | height: 4, |
| 173 | draw: list_long, |
| 174 | }, |
| 175 | Entry { |
| 176 | name: "list-empty", |
| 177 | width: 48, |
| 178 | height: 5, |
| 179 | draw: empty_list, |
| 180 | }, |
| 181 | Entry { |
| 182 | name: "empty-state", |
| 183 | width: 64, |
| 184 | height: 14, |
| 185 | draw: empty_state, |
| 186 | }, |
| 187 | Entry { |
| 188 | name: "empty-state-small", |
| 189 | width: 40, |
| 190 | height: 6, |
| 191 | draw: empty_state_small, |
| 192 | }, |
| 193 | ] |
| 194 | } |
| 195 |