Skip to content

Commit f2bbadc

Browse files
committed
refactor: unify interactive and non-interactive list rendering in vite_select
- Replace interactive_select() + print_select_list() with single select_list() - Add Mode enum: Interactive { selected_index: &mut usize } / NonInteractive - Remove SelectResult; selected index written directly via Mode reference - Shared render_items() with RenderParams used by both modes - Switch from crossterm styling to owo-colors with if_supports_color - Esc now clears search query instead of cancelling selection - Ctrl+C restores terminal and exits with code 130
1 parent 068b9f4 commit f2bbadc

9 files changed

Lines changed: 275 additions & 165 deletions

File tree

Cargo.lock

Lines changed: 33 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ nucleo-matcher = "0.3.1"
8989
once_cell = "1.19"
9090
os_str_bytes = "7.1.1"
9191
ouroboros = "0.18.5"
92-
owo-colors = "4.1.0"
92+
owo-colors = { version = "4.1.0", features = ["supports-colors"] }
9393
passfd = { git = "https://github.com/polachok/passfd", rev = "d55881752c16aced1a49a75f9c428d38d3767213", default-features = false }
9494
pathdiff = "0.2.3"
9595
petgraph = "0.8.2"

crates/vite_select/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ workspace = true
1414
anyhow = { workspace = true }
1515
crossterm = { workspace = true }
1616
nucleo-matcher = { workspace = true }
17+
owo-colors = { workspace = true }
1718
vite_str = { path = "../vite_str" }
1819

1920
[dev-dependencies]

crates/vite_select/src/interactive.rs

Lines changed: 119 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ use std::io::{Write, stdout};
33
use crossterm::{
44
cursor::{self, MoveToColumn},
55
event::{self, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers},
6-
style::{Attribute, Color, Print, ResetColor, SetAttribute, SetForegroundColor},
6+
style::{Attribute, SetAttribute},
77
terminal::{self, Clear, ClearType},
88
};
9+
use owo_colors::{OwoColorize, Stream};
910

10-
use crate::{RenderState, SelectItem, SelectResult, fuzzy::fuzzy_match};
11+
use crate::{RenderState, SelectItem, fuzzy::fuzzy_match};
1112

1213
struct RawModeGuard;
1314

@@ -97,125 +98,148 @@ impl<'a> State<'a> {
9798
}
9899
}
99100

100-
fn render(
101-
stdout: &mut impl Write,
102-
state: &mut State<'_>,
103-
header: Option<&str>,
104-
) -> anyhow::Result<()> {
105-
// Move cursor up to clear previous render
106-
if state.rendered_lines > 0 {
107-
let move_up = u16::try_from(state.rendered_lines)
108-
.expect("rendered_lines fits in u16: at most header + page_size + footer lines");
109-
crossterm::execute!(
110-
stdout,
111-
cursor::MoveUp(move_up),
112-
MoveToColumn(0),
113-
Clear(ClearType::FromCursorDown),
114-
)?;
115-
}
101+
/// Parameters for rendering a task list.
102+
pub struct RenderParams<'a> {
103+
pub items: &'a [SelectItem],
104+
pub filtered: &'a [usize],
105+
/// Index into `filtered` of the highlighted item, or `None` for non-interactive.
106+
pub selected_in_filtered: Option<usize>,
107+
/// Which slice of `filtered` to display.
108+
pub visible_range: std::ops::Range<usize>,
109+
/// Number of items beyond the visible range.
110+
pub hidden_count: usize,
111+
pub header: Option<&'a str>,
112+
/// Current search text. `Some` enables the prompt line (interactive only).
113+
pub query: Option<&'a str>,
114+
/// `"\r\n"` for raw mode, `"\n"` for normal.
115+
pub line_ending: &'a str,
116+
}
116117

117-
let mut lines = 0u16;
118+
/// Render the item list. Shared rendering logic used by both interactive
119+
/// and non-interactive modes (via [`crate::non_interactive`]).
120+
///
121+
/// Returns the number of lines written.
122+
pub fn render_items(writer: &mut impl Write, params: &RenderParams<'_>) -> anyhow::Result<usize> {
123+
let RenderParams {
124+
items,
125+
filtered,
126+
selected_in_filtered,
127+
visible_range,
128+
hidden_count,
129+
header,
130+
query,
131+
line_ending,
132+
} = params;
118133

119-
// Header (error message)
134+
let mut lines = 0usize;
135+
136+
// Header (e.g. error message)
120137
if let Some(header) = header {
121-
crossterm::execute!(stdout, Print(header), Print("\r\n"))?;
138+
write!(writer, "{header}{line_ending}")?;
122139
lines += 1;
123140
}
124141

125-
// Prompt line
126-
// Print ": " separator before query only when query is non-empty,
127-
// to avoid a trailing space that Windows ConPTY would strip.
128-
if state.query.is_empty() {
129-
crossterm::execute!(
130-
stdout,
131-
SetAttribute(Attribute::Bold),
132-
Print("Search task"),
133-
SetAttribute(Attribute::Reset),
134-
Print(" ("),
135-
Print("\u{2191}/\u{2193} to move, enter to select"),
136-
Print("):"),
137-
Print("\r\n"),
138-
)?;
139-
} else {
140-
crossterm::execute!(
141-
stdout,
142-
SetAttribute(Attribute::Bold),
143-
Print("Search task"),
144-
SetAttribute(Attribute::Reset),
145-
Print(" ("),
146-
Print("\u{2191}/\u{2193} to move, enter to select"),
147-
Print("): "),
148-
Print(&state.query),
149-
Print("\r\n"),
150-
)?;
142+
// Prompt line (interactive only)
143+
if let Some(q) = query {
144+
let bold = SetAttribute(Attribute::Bold);
145+
let reset = SetAttribute(Attribute::Reset);
146+
// Print ": " separator before query only when query is non-empty,
147+
// to avoid a trailing space that Windows ConPTY would strip.
148+
if q.is_empty() {
149+
write!(
150+
writer,
151+
"{bold}Search task{reset} (\u{2191}/\u{2193} to move, enter to select):{line_ending}",
152+
)?;
153+
} else {
154+
write!(
155+
writer,
156+
"{bold}Search task{reset} (\u{2191}/\u{2193} to move, enter to select): {q}{line_ending}",
157+
)?;
158+
}
159+
lines += 1;
151160
}
152-
lines += 1;
153161

154162
// Items
155-
let visible = state.visible_range();
156-
157-
for vi in visible {
158-
let item_idx = state.filtered[vi];
159-
let item = &state.items[item_idx];
160-
let is_selected = vi == state.selected;
163+
for vi in visible_range.clone() {
164+
let item_idx = filtered[vi];
165+
let item = &items[item_idx];
166+
let is_selected = *selected_in_filtered == Some(vi);
167+
let desc_str = item.description.as_str();
168+
let desc = desc_str.if_supports_color(Stream::Stdout, |s| s.cyan());
161169

162170
if is_selected {
163-
crossterm::execute!(
164-
stdout,
165-
SetAttribute(Attribute::Bold),
166-
Print("> "),
167-
Print(item.label.as_str()),
168-
Print(": "),
169-
SetForegroundColor(Color::Cyan),
170-
Print(item.description.as_str()),
171-
ResetColor,
172-
SetAttribute(Attribute::Reset),
173-
Print("\r\n"),
171+
write!(
172+
writer,
173+
"{bold}> {label}: {desc}{reset}{line_ending}",
174+
bold = SetAttribute(Attribute::Bold),
175+
label = item.label,
176+
reset = SetAttribute(Attribute::Reset),
174177
)?;
175178
} else {
176-
crossterm::execute!(
177-
stdout,
178-
Print(" "),
179-
Print(item.label.as_str()),
180-
Print(": "),
181-
SetForegroundColor(Color::Cyan),
182-
Print(item.description.as_str()),
183-
ResetColor,
184-
Print("\r\n"),
185-
)?;
179+
write!(writer, " {}: {desc}{line_ending}", item.label)?;
186180
}
187181
lines += 1;
188182
}
189183

190184
// Footer: hidden items count
191-
let hidden = state.hidden_count();
192-
if hidden > 0 {
193-
crossterm::execute!(
194-
stdout,
195-
Print(vite_str::format!(" (\u{2026}{hidden} more)")),
196-
Print("\r\n"),
197-
)?;
185+
if *hidden_count > 0 {
186+
write!(writer, " (\u{2026}{hidden_count} more){line_ending}")?;
198187
lines += 1;
199188
}
200189

201190
// Empty state
202-
if state.filtered.is_empty() {
203-
crossterm::execute!(stdout, Print(" No matching tasks.\r\n"))?;
191+
if filtered.is_empty() {
192+
write!(writer, " No matching tasks.{line_ending}")?;
204193
lines += 1;
205194
}
206195

207-
stdout.flush()?;
208-
state.rendered_lines = lines as usize;
196+
writer.flush()?;
197+
Ok(lines)
198+
}
199+
200+
fn render(
201+
stdout: &mut impl Write,
202+
state: &mut State<'_>,
203+
header: Option<&str>,
204+
) -> anyhow::Result<()> {
205+
// Move cursor up to clear previous render
206+
if state.rendered_lines > 0 {
207+
let move_up = u16::try_from(state.rendered_lines)
208+
.expect("rendered_lines fits in u16: at most header + page_size + footer lines");
209+
crossterm::execute!(
210+
stdout,
211+
cursor::MoveUp(move_up),
212+
MoveToColumn(0),
213+
Clear(ClearType::FromCursorDown),
214+
)?;
215+
}
216+
217+
let lines = render_items(
218+
stdout,
219+
&RenderParams {
220+
items: state.items,
221+
filtered: &state.filtered,
222+
selected_in_filtered: Some(state.selected),
223+
visible_range: state.visible_range(),
224+
hidden_count: state.hidden_count(),
225+
header,
226+
query: Some(&state.query),
227+
line_ending: "\r\n",
228+
},
229+
)?;
230+
231+
state.rendered_lines = lines;
209232
Ok(())
210233
}
211234

212235
pub fn run(
213236
items: &[SelectItem],
214237
initial_query: Option<&str>,
238+
selected_index: &mut usize,
215239
header: Option<&str>,
216240
page_size: usize,
217241
mut after_render: impl FnMut(&RenderState<'_>),
218-
) -> anyhow::Result<Option<SelectResult>> {
242+
) -> anyhow::Result<()> {
219243
if items.is_empty() {
220244
anyhow::bail!("No tasks available");
221245
}
@@ -236,19 +260,20 @@ pub fn run(
236260
match ev {
237261
Event::Key(KeyEvent { code, modifiers, kind: KeyEventKind::Press, .. }) => match code {
238262
KeyCode::Esc => {
239-
cleanup(&mut out, &state)?;
240-
return Ok(None);
263+
// Clear the search query and reset the filter
264+
state.query.clear();
265+
state.refilter();
241266
}
242267
KeyCode::Char('c') if modifiers.contains(KeyModifiers::CONTROL) => {
243268
cleanup(&mut out, &state)?;
244-
return Ok(None);
269+
std::process::exit(130);
245270
}
246271
KeyCode::Enter => {
247-
let result = state
248-
.selected_original_index()
249-
.map(|idx| SelectResult { original_index: idx });
272+
if let Some(idx) = state.selected_original_index() {
273+
*selected_index = idx;
274+
}
250275
cleanup(&mut out, &state)?;
251-
return Ok(result);
276+
return Ok(());
252277
}
253278
KeyCode::Up => {
254279
state.move_up();

0 commit comments

Comments
 (0)