|
1 | 1 | use crate::opt::{ConnectOpts, MigrationSourceOpt}; |
2 | 2 | use crate::{migrate, Config}; |
3 | | -use console::{style, Term}; |
4 | | -use dialoguer::Confirm; |
| 3 | +use console::style; |
5 | 4 | use sqlx::any::Any; |
6 | 5 | use sqlx::migrate::MigrateDatabase; |
7 | | -use std::{io, mem}; |
| 6 | +use std::io::{self, BufRead, Write}; |
8 | 7 | use tokio::task; |
9 | 8 |
|
10 | 9 | pub async fn create(connect_opts: &ConnectOpts) -> anyhow::Result<()> { |
@@ -66,45 +65,56 @@ pub async fn setup( |
66 | 65 | } |
67 | 66 |
|
68 | 67 | async fn ask_to_continue_drop(db_url: String) -> bool { |
69 | | - // If the setup operation is cancelled while we are waiting for the user to decide whether |
70 | | - // or not to drop the database, this will restore the terminal's cursor to its normal state. |
71 | | - struct RestoreCursorGuard { |
72 | | - disarmed: bool, |
73 | | - } |
| 68 | + task::spawn_blocking(move || { |
| 69 | + let stderr = io::stderr(); |
| 70 | + let mut stderr_lock = stderr.lock(); |
| 71 | + let _ = write!( |
| 72 | + stderr_lock, |
| 73 | + "Drop database at {}? (y/N): ", |
| 74 | + style(&db_url).cyan() |
| 75 | + ); |
| 76 | + let _ = stderr_lock.flush(); |
| 77 | + std::mem::drop(stderr_lock); |
74 | 78 |
|
75 | | - impl Drop for RestoreCursorGuard { |
76 | | - fn drop(&mut self) { |
77 | | - if !self.disarmed { |
78 | | - Term::stderr().show_cursor().unwrap() |
79 | | - } |
| 79 | + let stdin = io::stdin(); |
| 80 | + let mut line = String::new(); |
| 81 | + match stdin.lock().read_line(&mut line) { |
| 82 | + Ok(0) | Err(_) => false, |
| 83 | + Ok(_) => parse_drop_response(&line), |
80 | 84 | } |
81 | | - } |
82 | | - |
83 | | - let mut guard = RestoreCursorGuard { disarmed: false }; |
84 | | - |
85 | | - let decision_result = task::spawn_blocking(move || { |
86 | | - Confirm::new() |
87 | | - .with_prompt(format!("Drop database at {}?", style(&db_url).cyan())) |
88 | | - .wait_for_newline(true) |
89 | | - .default(false) |
90 | | - .show_default(true) |
91 | | - .interact() |
92 | 85 | }) |
93 | 86 | .await |
94 | | - .expect("Confirm thread panicked"); |
95 | | - match decision_result { |
96 | | - Ok(decision) => { |
97 | | - guard.disarmed = true; |
98 | | - decision |
99 | | - } |
100 | | - Err(dialoguer::Error::IO(err)) if err.kind() == io::ErrorKind::Interrupted => { |
101 | | - // Sometimes CTRL + C causes this error to be returned |
102 | | - mem::drop(guard); |
103 | | - false |
| 87 | + .expect("Confirm thread panicked") |
| 88 | +} |
| 89 | + |
| 90 | +fn parse_drop_response(line: &str) -> bool { |
| 91 | + let trimmed = line.trim(); |
| 92 | + trimmed.eq_ignore_ascii_case("y") || trimmed.eq_ignore_ascii_case("yes") |
| 93 | +} |
| 94 | + |
| 95 | +#[cfg(test)] |
| 96 | +mod tests { |
| 97 | + use super::parse_drop_response; |
| 98 | + |
| 99 | + #[test] |
| 100 | + fn parse_drop_response_accepts_yes() { |
| 101 | + for input in ["y", "Y", "yes", "YES", "Yes", "y\n", "yes\r\n", " yes "] { |
| 102 | + assert!( |
| 103 | + parse_drop_response(input), |
| 104 | + "expected yes for input {input:?}" |
| 105 | + ); |
104 | 106 | } |
105 | | - Err(err) => { |
106 | | - mem::drop(guard); |
107 | | - panic!("Confirm dialog failed with {err}") |
| 107 | + } |
| 108 | + |
| 109 | + #[test] |
| 110 | + fn parse_drop_response_rejects_everything_else() { |
| 111 | + for input in [ |
| 112 | + "", "\n", "\r\n", "n", "N", "no", "NO", "maybe", " ", "xyz", "yep", |
| 113 | + ] { |
| 114 | + assert!( |
| 115 | + !parse_drop_response(input), |
| 116 | + "expected no for input {input:?}" |
| 117 | + ); |
108 | 118 | } |
109 | 119 | } |
110 | 120 | } |
0 commit comments