Skip to content

Commit ef64ab8

Browse files
authored
update login ux (#915)
1 parent 927eca7 commit ef64ab8

File tree

3 files changed

+93
-7
lines changed

3 files changed

+93
-7
lines changed

crates/q_cli/src/cli/mod.rs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -422,12 +422,17 @@ impl Cli {
422422
pub async fn execute_chat(subcmd: &str, args: Option<Vec<String>>, enforce_login: bool) -> Result<ExitCode> {
423423
if enforce_login && !is_logged_in_check().await {
424424
if subcmd == "chat" {
425-
let options = ["Yes", "No"];
426-
match crate::util::choose(" You are not logged in. Login now?", &options)? {
427-
Some(0) => {},
428-
_ => bail!("Login is required to use chat"),
425+
match crate::util::welcome_login_prompt()? {
426+
true => {
427+
crate::cli::user::login_interactive(Default::default()).await?;
428+
},
429+
false => {
430+
bail!(
431+
"You are not logged in, please log in with {}",
432+
format!("{CLI_BINARY_NAME} login").bold()
433+
);
434+
},
429435
}
430-
crate::cli::user::login_interactive(Default::default()).await?;
431436
} else {
432437
bail!(
433438
"You are not logged in, please log in with {}",

crates/q_cli/src/cli/user.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -269,7 +269,7 @@ pub async fn login_interactive(args: LoginArgs) -> Result<()> {
269269

270270
let mut pre_portal_spinner = Spinner::new(vec![
271271
SpinnerComponent::Spinner,
272-
SpinnerComponent::Text(" Opening auth portal and logging in...".into()),
272+
SpinnerComponent::Text(crate::util::login_spinner_message()),
273273
]);
274274

275275
// Try unified portal, fallback to device flow on any error
@@ -291,7 +291,10 @@ pub async fn login_interactive(args: LoginArgs) -> Result<()> {
291291

292292
match finish_unified_portal(init, &secret_store).await? {
293293
PortalResult::Social(provider) => {
294-
pre_portal_spinner.stop_with_message(format!("Logged in with {}", provider));
294+
pre_portal_spinner.stop_with_message(format!(
295+
"\x1b[32m✓\x1b[0m Signed in with {} \x1b[90m(use \"{} logout\" to logout)\x1b[0m",
296+
provider, CLI_BINARY_NAME
297+
));
295298
Some(AuthMethod::Social(provider))
296299
},
297300
PortalResult::BuilderId { issuer_url, idc_region } => {

crates/q_cli/src/util/mod.rs

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,11 @@ use regex::Regex;
5353
pub use region_check::region_check;
5454
use tracing::warn;
5555

56+
// ANSI color codes
57+
const PURPLE: u8 = 93;
58+
const GRAY: u8 = 8;
59+
const MAGENTA: u8 = 13;
60+
5661
/// Glob patterns against full paths
5762
pub fn glob_dir(glob: &GlobSet, directory: impl AsRef<Path>) -> Result<Vec<PathBuf>> {
5863
let mut files = Vec::new();
@@ -276,6 +281,71 @@ pub fn choose(prompt: impl Display, options: &[impl ToString]) -> Result<Option<
276281
}
277282
}
278283

284+
pub fn welcome_login_prompt() -> Result<bool> {
285+
use crossterm::event::{
286+
self,
287+
Event,
288+
KeyCode,
289+
KeyEvent,
290+
};
291+
use crossterm::style::{
292+
Color,
293+
ResetColor,
294+
SetForegroundColor,
295+
};
296+
use crossterm::terminal::{
297+
disable_raw_mode,
298+
enable_raw_mode,
299+
};
300+
301+
if !stdout().is_terminal() {
302+
warn!("called welcome_login_prompt while stdout is not a terminal");
303+
return Ok(true);
304+
}
305+
306+
println!();
307+
print!("Welcome to ");
308+
print!(
309+
"{}{}{}",
310+
SetForegroundColor(Color::AnsiValue(PURPLE)),
311+
PRODUCT_NAME,
312+
ResetColor
313+
);
314+
println!(", let's get you signed in!");
315+
println!();
316+
317+
// Print instruction line with styling
318+
print!("{}Press ", SetForegroundColor(Color::AnsiValue(GRAY)));
319+
print!(
320+
"{}enter{}",
321+
SetForegroundColor(Color::AnsiValue(MAGENTA)),
322+
SetForegroundColor(Color::AnsiValue(GRAY))
323+
);
324+
print!(" to continue to the browser or ");
325+
print!(
326+
"{}esc{}",
327+
SetForegroundColor(Color::AnsiValue(MAGENTA)),
328+
SetForegroundColor(Color::AnsiValue(GRAY))
329+
);
330+
println!(" to cancel{}", ResetColor);
331+
println!();
332+
333+
enable_raw_mode()?;
334+
let result = loop {
335+
if let Ok(Event::Key(KeyEvent { code, .. })) = event::read() {
336+
match code {
337+
KeyCode::Enter => break Ok(true),
338+
KeyCode::Esc => break Ok(false),
339+
KeyCode::Char('c') if event::read().is_ok() => break Ok(false), // Ctrl+C
340+
_ => {},
341+
}
342+
}
343+
};
344+
disable_raw_mode()?;
345+
346+
result
347+
}
348+
279349
pub fn input(prompt: &str, initial_text: Option<&str>) -> Result<String> {
280350
if !stdout().is_terminal() {
281351
warn!("called input while stdout is not a terminal");
@@ -319,6 +389,14 @@ pub fn dialoguer_theme() -> ColorfulTheme {
319389
..ColorfulTheme::default()
320390
}
321391
}
392+
393+
pub fn login_spinner_message() -> String {
394+
format!(
395+
" \x1b[38;5;{}mOpening browser... | Press \x1b[38;5;{}m(^) + C\x1b[38;5;{}m to cancel\x1b[0m",
396+
GRAY, MAGENTA, GRAY
397+
)
398+
}
399+
322400
pub async fn is_logged_in_check() -> bool {
323401
std::env::var("AMAZON_Q_SIGV4").is_ok_and(|v| !v.is_empty()) || fig_auth::is_logged_in().await
324402
}

0 commit comments

Comments
 (0)