generated from pure_sagacity/rust-starter
added the provider trait, github provider, and a fzf selector.
i forgot to commit :(
This commit is contained in:
Generated
+3568
-1
File diff suppressed because it is too large
Load Diff
@@ -4,3 +4,11 @@ version = "0.1.0"
|
|||||||
edition = "2024"
|
edition = "2024"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
|
reqwest = { version = "0.13.4", features = ["json"] }
|
||||||
|
clap = { version = "4.6.1", features = ["derive"] }
|
||||||
|
tokio = { version = "1.52.3", features = ["full"] }
|
||||||
|
serde_json = "1.0.150"
|
||||||
|
octocrab = "0.54.0"
|
||||||
|
dialoguer = "0.12"
|
||||||
|
keyring = "4.1.4"
|
||||||
|
|||||||
+41
-2
@@ -1,3 +1,42 @@
|
|||||||
fn main() {
|
use crate::{
|
||||||
println!("Hello, world!");
|
providers::{Provider, github::Github},
|
||||||
|
tui::user_select_repo,
|
||||||
|
};
|
||||||
|
use dialoguer::{Select, theme::ColorfulTheme};
|
||||||
|
mod providers;
|
||||||
|
mod tui;
|
||||||
|
|
||||||
|
pub fn ask_yes_no(message: &str) -> bool {
|
||||||
|
let options = vec!["Yes", "No"];
|
||||||
|
|
||||||
|
let selection = Select::with_theme(&ColorfulTheme::default())
|
||||||
|
.with_prompt(message.to_string())
|
||||||
|
.items(&options)
|
||||||
|
.default(0)
|
||||||
|
.interact()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
match options[selection] {
|
||||||
|
"Yes" => true,
|
||||||
|
"No" => false,
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::main]
|
||||||
|
async fn main() {
|
||||||
|
// Testing to see if github provider works.
|
||||||
|
let gh = Github {
|
||||||
|
username: "pure-sagacity".to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let repos = gh.repositories().await.expect("Failed to get repos.");
|
||||||
|
|
||||||
|
let selection = user_select_repo(repos)
|
||||||
|
.await
|
||||||
|
.expect("Failed to select repository.");
|
||||||
|
|
||||||
|
println!("{:#?}", selection);
|
||||||
|
|
||||||
|
println!("{}", ask_yes_no("Clone the repo?"));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
use super::Repository;
|
||||||
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use crate::providers::Provider;
|
||||||
|
|
||||||
|
// TODO: add email and password authentication for viewing private repos.
|
||||||
|
pub struct Github {
|
||||||
|
pub username: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. This struct matches GitHub's exact API shape.
|
||||||
|
// It remains completely hidden inside your GitHub provider module.
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct GithubRepoDto {
|
||||||
|
id: u64,
|
||||||
|
name: String,
|
||||||
|
owner: GithubOwnerDto,
|
||||||
|
ssh_url: Option<String>,
|
||||||
|
clone_url: String, // Or html_url
|
||||||
|
stargazers_count: u64,
|
||||||
|
description: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct GithubOwnerDto {
|
||||||
|
login: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Map the GitHub DTO to your general Repository struct
|
||||||
|
impl From<GithubRepoDto> for Repository {
|
||||||
|
fn from(gh: GithubRepoDto) -> Self {
|
||||||
|
Repository {
|
||||||
|
id: gh.id,
|
||||||
|
name: gh.name,
|
||||||
|
owner: gh.owner.login,
|
||||||
|
ssh_url: gh.ssh_url,
|
||||||
|
https_url: gh.clone_url,
|
||||||
|
stars: gh.stargazers_count,
|
||||||
|
description: gh.description,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Provider for Github {
|
||||||
|
async fn repositories(&self) -> super::Result<Vec<Repository>> {
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let mut page = 1;
|
||||||
|
let mut repos: Vec<Repository> = Vec::new();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let url = format!(
|
||||||
|
"https://api.github.com/users/{}/repos?per_page=100&page={}",
|
||||||
|
&self.username, page
|
||||||
|
);
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.get(&url)
|
||||||
|
.header("User-Agent", "my-app")
|
||||||
|
.send()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let status = response.status();
|
||||||
|
|
||||||
|
if !status.is_success() {
|
||||||
|
let error_text = response.text().await?;
|
||||||
|
let err = format!("Github API Error ({}): {}", status, error_text);
|
||||||
|
return Err(err.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let gh_page: Vec<GithubRepoDto> = response.json().await?;
|
||||||
|
|
||||||
|
if gh_page.is_empty() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the GitHub DTOs seamlessly into your clean domain structs
|
||||||
|
let domain_repos = gh_page.into_iter().map(Repository::from);
|
||||||
|
repos.extend(domain_repos);
|
||||||
|
|
||||||
|
page += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(repos)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
pub mod github;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use tokio::process::Command;
|
||||||
|
|
||||||
|
pub type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct Repository {
|
||||||
|
pub id: u64,
|
||||||
|
pub name: String,
|
||||||
|
pub owner: String,
|
||||||
|
pub ssh_url: Option<String>,
|
||||||
|
pub https_url: String,
|
||||||
|
pub stars: u64,
|
||||||
|
pub description: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Provider {
|
||||||
|
async fn repositories(&self) -> Result<Vec<Repository>>;
|
||||||
|
|
||||||
|
async fn clone(&self, repo: Repository, folder: Option<String>, use_ssh: bool) -> Result<()> {
|
||||||
|
let mut cmd = Command::new("git");
|
||||||
|
cmd.arg("clone");
|
||||||
|
|
||||||
|
match use_ssh {
|
||||||
|
true => match repo.ssh_url {
|
||||||
|
Some(u) => cmd.arg(u),
|
||||||
|
None => return Err("Repo doesn't have an SSH URL.".into()),
|
||||||
|
},
|
||||||
|
false => cmd.arg(repo.https_url),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(f) = folder {
|
||||||
|
cmd.arg(f);
|
||||||
|
}
|
||||||
|
|
||||||
|
match cmd.status().await {
|
||||||
|
Ok(status) if status.success() => {
|
||||||
|
println!("Git clone completed successfully!");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Ok(status) => {
|
||||||
|
// The command ran, but exited with an error code (e.g., 128)
|
||||||
|
Err(format!("Git clone failed with exit code: {}", status).into())
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
// The command couldn't run at all (e.g., 'git' isn't installed)
|
||||||
|
Err(format!("Failed to execute git process: {}", e).into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
use crate::providers::{Repository, Result};
|
||||||
|
use std::process::Stdio;
|
||||||
|
use tokio::io::{AsyncReadExt, AsyncWriteExt};
|
||||||
|
use tokio::process::Command;
|
||||||
|
|
||||||
|
pub async fn user_select_repo(repositories: Vec<Repository>) -> Result<Repository> {
|
||||||
|
let mut fzf_input = String::new();
|
||||||
|
for (index, repo) in repositories.iter().enumerate() {
|
||||||
|
let line = format!(
|
||||||
|
"{}|{}/{}|{}|{}|{}\n",
|
||||||
|
index,
|
||||||
|
repo.owner,
|
||||||
|
repo.name,
|
||||||
|
repo.id,
|
||||||
|
repo.stars,
|
||||||
|
repo.description
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or("No description available")
|
||||||
|
);
|
||||||
|
fzf_input.push_str(&line);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut child = Command::new("fzf")
|
||||||
|
.arg("--delimiter").arg("\\|")
|
||||||
|
.arg("--with-nth").arg("2")
|
||||||
|
.arg("--preview")
|
||||||
|
.arg("echo 'Repository: {2}\n-------------------\nID: {3}\nStars: ⭐ {4}\nDescription: {5}'")
|
||||||
|
.arg("--preview-window").arg("right:50%:wrap")
|
||||||
|
.stdin(Stdio::piped())
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.spawn()?;
|
||||||
|
|
||||||
|
if let Some(mut stdin) = child.stdin.take() {
|
||||||
|
stdin.write_all(fzf_input.as_bytes()).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut selection = String::new();
|
||||||
|
if let Some(mut stdout) = child.stdout.take() {
|
||||||
|
stdout.read_to_string(&mut selection).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let status = child.wait().await?;
|
||||||
|
if !status.success() {
|
||||||
|
return Err("fzf closed or cancelled".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the index from the beginning of the selected line
|
||||||
|
let first_field = selection
|
||||||
|
.split('|')
|
||||||
|
.next()
|
||||||
|
.ok_or("Invalid selection format")?
|
||||||
|
.trim();
|
||||||
|
let index: usize = first_field.parse()?;
|
||||||
|
|
||||||
|
// Move the repository out of the vector and return it
|
||||||
|
// (Consumes the vector item matching the index chosen)
|
||||||
|
let mut repos = repositories;
|
||||||
|
if index < repos.len() {
|
||||||
|
Ok(repos.remove(index))
|
||||||
|
} else {
|
||||||
|
Err("Selected index out of bounds".into())
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user