use std::{fs, io, process::Command, rc::Rc};
use clap::CommandFactory;
use clap_complete::{
Shell,
aot::{Bash, Fish, Zsh},
generate,
};
use dialoguer::{Select, theme::ColorfulTheme};
use crate::{
auth::{LeetCodeCredentials, auto_extract_flow, manual_auth_flow},
client::LeetCodeClient,
config::CONFIG,
models::{Identifier, Language, ProblemSummary},
picker::Picker,
};
pub async fn make_picker() -> std::result::Result<Picker, String> {
let creds = LeetCodeCredentials::load()
.ok_or_else(|| "Not authenticated. Please run `leetrs auth` first.".to_string())?;
let client =
LeetCodeClient::new(creds).map_err(|e| format!("Failed to initialize client: {}", e))?;
Ok(Picker::new(client))
}
pub fn handle_auth() {
println!("🔒 LeetCode Authentication\n");
let options = &[
"Paste tokens manually",
"Extract from Firefox",
"Extract from Chrome",
];
let selection = Select::with_theme(&ColorfulTheme::default())
.with_prompt("How would you like to authenticate?")
.default(0)
.items(&options[..])
.interact()
.unwrap();
let credentials_result = match selection {
0 => manual_auth_flow(),
1 => auto_extract_flow("firefox"),
2 => auto_extract_flow("chrome"),
_ => unreachable!(),
};
match credentials_result {
Ok(creds) => match creds.save() {
Ok(_) => println!("\n✅ Authentication successful!"),
Err(e) => eprintln!("\n❌ Failed to save credentials: {}", e),
},
Err(e) => {
eprintln!("\n❌ Authentication failed: {}", e);
if selection != 0 {
eprintln!(
"Tip: Make sure you are logged into leetcode.com on that browser, or try the manual option."
);
}
}
}
}
pub fn handle_status() {
match LeetCodeCredentials::load() {
Some(creds) => {
println!("✅ Currently authenticated!");
println!("🔑 csrftoken:");
println!("{}\n", creds.csrf_token);
println!("🔑 LEETCODE_SESSION:");
println!("{}", creds.session_cookie);
}
None => {
eprintln!("❌ Not authenticated. No valid credentials found.");
eprintln!("Run `leetrs auth` to set up your account.");
}
}
}
pub async fn handle_pick(
identifier: &Identifier,
language: &Option<Language>,
preview: bool,
) -> std::result::Result<(), String> {
let picker = make_picker().await?;
pick_and_open(&picker, identifier, language, preview).await
}
pub async fn handle_test(file: &str) -> std::result::Result<(), String> {
let picker = make_picker().await?;
picker.test_submit(file).await;
Ok(())
}
pub async fn handle_submit(file: &str) -> std::result::Result<(), String> {
let picker = make_picker().await?;
picker.submit(file).await;
Ok(())
}
pub fn handle_completion<C: CommandFactory>(shell: &Shell) {
let mut cmd = C::command();
match shell {
Shell::Bash => generate(Bash, &mut cmd, "leetrs", &mut io::stdout()),
Shell::Zsh => generate(Zsh, &mut cmd, "leetrs", &mut io::stdout()),
Shell::Fish => generate(Fish, &mut cmd, "leetrs", &mut io::stdout()),
Shell::Elvish => todo!(),
Shell::PowerShell => todo!(),
_ => todo!(),
}
}
pub async fn open_tui(language: &Option<Language>) {
let creds = match LeetCodeCredentials::load() {
Some(c) => c,
None => {
eprintln!("❌ Not authenticated. Please run `leetrs auth` first.");
return;
}
};
let client = match LeetCodeClient::new(creds) {
Ok(c) => c,
Err(e) => {
eprintln!("❌ Failed to initialize client: {}", e);
return;
}
};
let picker = Picker::new(client);
let problems = match picker.list_problems().await {
Ok(p) => p,
Err(e) => {
eprintln!("❌ Failed to fetch problems: {}", e);
return;
}
};
let problems: Rc<[ProblemSummary]> = Rc::from(problems);
let user_data = picker.get_user_data().await.ok();
if let Err(e) = crate::tui::run_tui(Rc::clone(&problems), picker, user_data, language).await {
eprintln!("❌ TUI error: {}", e);
}
}
pub async fn pick_and_open(
picker: &Picker,
identifier: &Identifier,
language: &Option<Language>,
preview: bool,
) -> std::result::Result<(), String> {
let (code, desc) = picker
.pick(identifier, language)
.await
.map_err(|e| format!("{}", e))?;
if !preview {
let config = CONFIG
.get()
.ok_or_else(|| "Failed to initialise config".to_string())?;
let editor = config.editor.as_deref().unwrap_or("nvim");
let show_description = config.show_description.unwrap_or(true);
println!("🚀 launching {}...", editor);
let status = if show_description {
if editor.contains("nvim") || editor.contains("vim") {
Command::new(editor)
.arg(&desc)
.arg("-c")
.arg(format!("vsplit {}", code))
.status()
} else {
Command::new(editor).arg(&desc).arg(&code).status()
}
} else {
Command::new(editor).arg(&code).status()
};
match status {
Ok(exit_status) if exit_status.success() => {
println!("\n👋 {} closed.", editor);
}
Ok(exit_status) => {
eprintln!("⚠️ {} exited with an error code: {}", editor, exit_status);
}
Err(e) => {
eprintln!(
"❌ failed to launch {}. is it installed and in your path? error: {}",
editor, e
);
}
}
} else {
let content = fs::read_to_string(desc)
.map_err(|e| format!("Failed to read description file: {}", e))?;
print!("{}", content);
}
Ok(())
}