222 lines
7.7 KiB
Rust
222 lines
7.7 KiB
Rust
use crossterm::{
|
|
cursor, event, execute,
|
|
style::{self, Color, Print, SetForegroundColor},
|
|
terminal,
|
|
};
|
|
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
|
|
use std::io::{self, Write};
|
|
use std::time::Duration;
|
|
|
|
pub fn create_progress_bar(
|
|
multi: &MultiProgress,
|
|
) -> (ProgressBar, impl Fn(&str, &str, usize, usize) + '_) {
|
|
let pb = multi.add(ProgressBar::new(0));
|
|
pb.enable_steady_tick(Duration::from_millis(50));
|
|
pb.set_style(
|
|
ProgressStyle::default_bar()
|
|
.template("> {spinner:.blue} {prefix}")
|
|
.unwrap(),
|
|
);
|
|
|
|
let pb_clone = pb.clone();
|
|
let callback = move |prefix: &str, msg: &str, progress: usize, total: usize| {
|
|
let pb = &pb_clone;
|
|
if progress != 0 && total != 0 {
|
|
pb.set_style(ProgressStyle::default_bar()
|
|
.template("> {spinner:.blue} {prefix}\n {msg} [{bar:40.cyan/blue}] {pos}/{len} ({eta})")
|
|
.unwrap()
|
|
.progress_chars("=> "));
|
|
} else {
|
|
pb.set_style(
|
|
ProgressStyle::default_bar()
|
|
.template("> {spinner:.blue} {prefix}")
|
|
.unwrap(),
|
|
);
|
|
}
|
|
|
|
if !prefix.is_empty() {
|
|
pb.set_prefix(prefix.to_string());
|
|
}
|
|
|
|
pb.set_message(msg.to_string());
|
|
pb.set_length(total as u64);
|
|
pb.set_position(progress as u64);
|
|
};
|
|
|
|
(pb, callback)
|
|
}
|
|
|
|
const SELECT_PROMPT: &str = "> Select series: ";
|
|
|
|
/// Interactive one-line series selector with arrow key navigation and direct typing.
|
|
///
|
|
/// The user can:
|
|
/// - Navigate with ↑/↓ arrow keys to cycle through options
|
|
/// - Type directly to enter a custom release name
|
|
/// - Press Tab to cycle through options matching the current input prefix
|
|
/// - Press Enter to confirm, Escape to use the default
|
|
///
|
|
/// Returns the selected series name, or an error if cancelled (Ctrl+C).
|
|
pub fn select_series(
|
|
options: &[String],
|
|
default: &str,
|
|
) -> Result<String, Box<dyn std::error::Error>> {
|
|
if options.is_empty() {
|
|
println!("{}{} (default)", SELECT_PROMPT, default);
|
|
return Ok(default.to_string());
|
|
}
|
|
|
|
// Try to enter raw mode — if not possible (e.g. piped input), fall back to default
|
|
if terminal::enable_raw_mode().is_err() {
|
|
println!("{}{} (default)", SELECT_PROMPT, default);
|
|
return Ok(default.to_string());
|
|
}
|
|
|
|
let result = select_series_inner(options, default);
|
|
|
|
// Always restore the terminal
|
|
let _ = terminal::disable_raw_mode();
|
|
|
|
result
|
|
}
|
|
|
|
fn select_series_inner(
|
|
options: &[String],
|
|
default: &str,
|
|
) -> Result<String, Box<dyn std::error::Error>> {
|
|
let default_idx = options.iter().position(|o| o == default).unwrap_or(0);
|
|
let mut selected_idx = default_idx;
|
|
let mut input = default.to_string();
|
|
// Whether we are in "navigation mode" (last action was arrow/tab selecting an option)
|
|
// vs "typing mode" (last action was typing a character)
|
|
let mut navigating = true;
|
|
|
|
let prompt_len = SELECT_PROMPT.len();
|
|
|
|
let mut stdout = io::stdout();
|
|
|
|
// Draw the prompt line, clearing any previous content
|
|
let draw = |stdout: &mut io::Stdout, input: &str| -> Result<(), Box<dyn std::error::Error>> {
|
|
execute!(
|
|
stdout,
|
|
cursor::MoveToColumn(0),
|
|
Print(SELECT_PROMPT),
|
|
SetForegroundColor(Color::Cyan),
|
|
Print(input),
|
|
style::ResetColor,
|
|
terminal::Clear(terminal::ClearType::UntilNewLine),
|
|
)?;
|
|
stdout.flush()?;
|
|
Ok(())
|
|
};
|
|
|
|
draw(&mut stdout, &input)?;
|
|
|
|
loop {
|
|
if event::poll(Duration::from_millis(200))?
|
|
&& let event::Event::Key(event::KeyEvent {
|
|
code, modifiers, ..
|
|
}) = event::read()?
|
|
{
|
|
match code {
|
|
event::KeyCode::Up => {
|
|
navigating = true;
|
|
if selected_idx > 0 {
|
|
selected_idx -= 1;
|
|
} else {
|
|
selected_idx = options.len() - 1;
|
|
}
|
|
input = options[selected_idx].clone();
|
|
draw(&mut stdout, &input)?;
|
|
}
|
|
event::KeyCode::Down => {
|
|
navigating = true;
|
|
if selected_idx < options.len() - 1 {
|
|
selected_idx += 1;
|
|
} else {
|
|
selected_idx = 0;
|
|
}
|
|
input = options[selected_idx].clone();
|
|
draw(&mut stdout, &input)?;
|
|
}
|
|
event::KeyCode::Enter => {
|
|
break;
|
|
}
|
|
event::KeyCode::Esc => {
|
|
input = default.to_string();
|
|
break;
|
|
}
|
|
event::KeyCode::Char(c) => {
|
|
if modifiers.contains(event::KeyModifiers::CONTROL) && c == 'c' {
|
|
// Move to a new line before returning so the terminal isn't messed up
|
|
execute!(stdout, Print("\r\n"))?;
|
|
return Err("Cancelled".into());
|
|
}
|
|
navigating = false;
|
|
input.push(c);
|
|
// Auto-select if the input exactly matches an option
|
|
if let Some(idx) = options.iter().position(|o| o == &input) {
|
|
selected_idx = idx;
|
|
navigating = true;
|
|
}
|
|
draw(&mut stdout, &input)?;
|
|
}
|
|
event::KeyCode::Backspace => {
|
|
if !input.is_empty() {
|
|
input.pop();
|
|
navigating = false;
|
|
if let Some(idx) = options.iter().position(|o| o == &input) {
|
|
selected_idx = idx;
|
|
navigating = true;
|
|
}
|
|
// Need to redraw — move cursor to end of prompt + input
|
|
execute!(
|
|
stdout,
|
|
cursor::MoveToColumn(prompt_len as u16),
|
|
Print(&input),
|
|
terminal::Clear(terminal::ClearType::UntilNewLine),
|
|
)?;
|
|
stdout.flush()?;
|
|
}
|
|
}
|
|
event::KeyCode::Tab => {
|
|
// Cycle through options that start with the current input
|
|
let matches: Vec<usize> = options
|
|
.iter()
|
|
.enumerate()
|
|
.filter(|(_, o)| o.starts_with(&input))
|
|
.map(|(i, _)| i)
|
|
.collect();
|
|
if !matches.is_empty() {
|
|
let next = if navigating {
|
|
matches
|
|
.iter()
|
|
.find(|&&i| i > selected_idx)
|
|
.or_else(|| matches.first())
|
|
} else {
|
|
matches.first()
|
|
};
|
|
if let Some(&idx) = next {
|
|
selected_idx = idx;
|
|
input = options[idx].clone();
|
|
navigating = true;
|
|
}
|
|
draw(&mut stdout, &input)?;
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Move to a new line so subsequent output doesn't overwrite the prompt
|
|
execute!(
|
|
stdout,
|
|
terminal::Clear(terminal::ClearType::UntilNewLine),
|
|
Print("\r\n"),
|
|
)?;
|
|
stdout.flush()?;
|
|
|
|
Ok(input)
|
|
}
|