2024-05-07 20:28:17 +02:00
|
|
|
use std::io::IsTerminal;
|
|
|
|
|
2023-07-11 18:43:19 +02:00
|
|
|
use clap::{Parser, Subcommand};
|
2024-04-18 23:47:22 +02:00
|
|
|
use eyre::{eyre, Context, OptionExt};
|
2023-07-11 18:43:19 +02:00
|
|
|
use tokio::io::AsyncWriteExt;
|
|
|
|
|
2023-08-17 23:56:23 +02:00
|
|
|
mod keys;
|
|
|
|
use keys::*;
|
|
|
|
|
2023-11-17 20:18:48 +01:00
|
|
|
mod auth;
|
2023-12-11 05:59:04 +01:00
|
|
|
mod issues;
|
2024-04-30 19:08:10 +02:00
|
|
|
mod prs;
|
2023-12-17 01:42:38 +01:00
|
|
|
mod release;
|
2023-11-17 20:25:35 +01:00
|
|
|
mod repo;
|
2023-11-17 20:18:48 +01:00
|
|
|
|
2023-07-11 18:43:19 +02:00
|
|
|
#[derive(Parser, Debug)]
|
|
|
|
pub struct App {
|
2024-04-17 21:41:06 +02:00
|
|
|
#[clap(long, short = 'H')]
|
|
|
|
host: Option<String>,
|
2024-05-07 20:28:17 +02:00
|
|
|
#[clap(long)]
|
|
|
|
style: Option<Style>,
|
2023-07-11 18:43:19 +02:00
|
|
|
#[clap(subcommand)]
|
|
|
|
command: Command,
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Subcommand, Clone, Debug)]
|
|
|
|
pub enum Command {
|
|
|
|
#[clap(subcommand)]
|
2023-11-17 20:18:48 +01:00
|
|
|
Repo(repo::RepoCommand),
|
2023-12-11 05:59:04 +01:00
|
|
|
Issue(issues::IssueCommand),
|
2024-04-30 19:08:10 +02:00
|
|
|
Pr(prs::PrCommand),
|
2024-04-19 00:53:36 +02:00
|
|
|
#[command(name = "whoami")]
|
|
|
|
WhoAmI {
|
2023-07-11 18:43:19 +02:00
|
|
|
#[clap(long, short)]
|
2024-04-17 21:41:06 +02:00
|
|
|
remote: Option<String>,
|
2023-07-11 18:43:19 +02:00
|
|
|
},
|
|
|
|
#[clap(subcommand)]
|
2023-11-17 20:18:48 +01:00
|
|
|
Auth(auth::AuthCommand),
|
2023-12-17 01:42:38 +01:00
|
|
|
Release(release::ReleaseCommand),
|
2023-07-11 18:43:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
#[tokio::main]
|
|
|
|
async fn main() -> eyre::Result<()> {
|
|
|
|
let args = App::parse();
|
2024-05-07 20:28:17 +02:00
|
|
|
|
|
|
|
let _ = SPECIAL_RENDER.set(SpecialRender::new(args.style.unwrap_or_default()));
|
|
|
|
|
2023-07-11 18:43:19 +02:00
|
|
|
let mut keys = KeyInfo::load().await?;
|
|
|
|
|
2024-04-17 21:41:06 +02:00
|
|
|
let host_name = args.host.as_deref();
|
|
|
|
// let remote = repo::RepoInfo::get_current(host_name, remote_name)?;
|
2023-07-11 18:43:19 +02:00
|
|
|
match args.command {
|
2024-05-31 21:25:59 +02:00
|
|
|
Command::Repo(subcommand) => subcommand.run(&mut keys, host_name).await?,
|
|
|
|
Command::Issue(subcommand) => subcommand.run(&mut keys, host_name).await?,
|
|
|
|
Command::Pr(subcommand) => subcommand.run(&mut keys, host_name).await?,
|
2024-04-19 00:53:36 +02:00
|
|
|
Command::WhoAmI { remote } => {
|
2024-04-17 21:58:45 +02:00
|
|
|
let url = repo::RepoInfo::get_current(host_name, None, remote.as_deref())
|
|
|
|
.wrap_err("could not find host, try specifying with --host")?
|
|
|
|
.host_url()
|
|
|
|
.clone();
|
2023-11-17 20:50:08 +01:00
|
|
|
let name = keys.get_login(&url)?.username();
|
2024-04-18 23:47:22 +02:00
|
|
|
let host = url
|
|
|
|
.host_str()
|
|
|
|
.ok_or_eyre("instance url does not have host")?;
|
|
|
|
if url.path() == "/" || url.path().is_empty() {
|
|
|
|
println!("currently signed in to {name}@{host}");
|
|
|
|
} else {
|
|
|
|
println!("currently signed in to {name}@{host}{}", url.path());
|
|
|
|
}
|
2023-07-20 22:06:16 +02:00
|
|
|
}
|
2023-12-11 05:59:04 +01:00
|
|
|
Command::Auth(subcommand) => subcommand.run(&mut keys).await?,
|
2024-04-17 21:41:06 +02:00
|
|
|
Command::Release(subcommand) => subcommand.run(&mut keys, host_name).await?,
|
2023-07-11 18:43:19 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
keys.save().await?;
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
async fn readline(msg: &str) -> eyre::Result<String> {
|
|
|
|
print!("{msg}");
|
|
|
|
tokio::io::stdout().flush().await?;
|
|
|
|
tokio::task::spawn_blocking(|| {
|
|
|
|
let mut input = String::new();
|
|
|
|
std::io::stdin().read_line(&mut input)?;
|
|
|
|
Ok(input)
|
|
|
|
})
|
|
|
|
.await?
|
|
|
|
}
|
2023-12-11 05:59:04 +01:00
|
|
|
|
|
|
|
async fn editor(contents: &mut String, ext: Option<&str>) -> eyre::Result<()> {
|
2023-12-13 06:30:47 +01:00
|
|
|
let editor = std::path::PathBuf::from(
|
|
|
|
std::env::var_os("EDITOR").ok_or_else(|| eyre!("unable to locate editor"))?,
|
|
|
|
);
|
2023-12-13 02:02:37 +01:00
|
|
|
|
2023-12-11 05:59:04 +01:00
|
|
|
let (mut file, path) = tempfile(ext).await?;
|
|
|
|
file.write_all(contents.as_bytes()).await?;
|
|
|
|
drop(file);
|
|
|
|
|
|
|
|
// Closure acting as a try/catch block so that the temp file is deleted even
|
|
|
|
// on errors
|
|
|
|
let res = (|| async {
|
|
|
|
eprint!("waiting on editor\r");
|
2023-12-13 06:25:28 +01:00
|
|
|
let flags = get_editor_flags(&editor);
|
2023-12-11 05:59:04 +01:00
|
|
|
let status = tokio::process::Command::new(editor)
|
2023-12-13 06:25:28 +01:00
|
|
|
.args(flags)
|
2023-12-11 05:59:04 +01:00
|
|
|
.arg(&path)
|
|
|
|
.status()
|
|
|
|
.await?;
|
|
|
|
if !status.success() {
|
|
|
|
eyre::bail!("editor exited unsuccessfully");
|
|
|
|
}
|
|
|
|
|
|
|
|
*contents = tokio::fs::read_to_string(&path).await?;
|
|
|
|
eprint!(" \r");
|
|
|
|
|
|
|
|
Ok(())
|
2023-12-13 02:02:37 +01:00
|
|
|
})()
|
|
|
|
.await;
|
2023-12-11 05:59:04 +01:00
|
|
|
|
|
|
|
tokio::fs::remove_file(path).await?;
|
|
|
|
res?;
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2023-12-13 06:25:28 +01:00
|
|
|
fn get_editor_flags(editor_path: &std::path::Path) -> &'static [&'static str] {
|
|
|
|
let editor_name = match editor_path.file_stem().and_then(|s| s.to_str()) {
|
|
|
|
Some(name) => name,
|
|
|
|
None => return &[],
|
|
|
|
};
|
|
|
|
if editor_name == "code" {
|
|
|
|
return &["--wait"];
|
|
|
|
}
|
|
|
|
&[]
|
|
|
|
}
|
|
|
|
|
2023-12-11 05:59:04 +01:00
|
|
|
async fn tempfile(ext: Option<&str>) -> tokio::io::Result<(tokio::fs::File, std::path::PathBuf)> {
|
|
|
|
let filename = uuid::Uuid::new_v4();
|
|
|
|
let mut path = std::env::temp_dir().join(filename.to_string());
|
|
|
|
if let Some(ext) = ext {
|
|
|
|
path.set_extension(ext);
|
|
|
|
}
|
|
|
|
let file = tokio::fs::OpenOptions::new()
|
|
|
|
.create(true)
|
|
|
|
.read(true)
|
|
|
|
.write(true)
|
|
|
|
.open(&path)
|
|
|
|
.await?;
|
|
|
|
Ok((file, path))
|
|
|
|
}
|
2024-05-07 20:28:17 +02:00
|
|
|
|
|
|
|
use std::sync::OnceLock;
|
|
|
|
static SPECIAL_RENDER: OnceLock<SpecialRender> = OnceLock::new();
|
|
|
|
|
|
|
|
fn special_render() -> &'static SpecialRender {
|
|
|
|
SPECIAL_RENDER
|
|
|
|
.get()
|
|
|
|
.expect("attempted to get special characters before that was initialized")
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(clap::ValueEnum, Clone, Copy, Debug, Default)]
|
|
|
|
enum Style {
|
|
|
|
/// Use special characters, and colors.
|
|
|
|
#[default]
|
|
|
|
Fancy,
|
|
|
|
/// No special characters and no colors. Always used in non-terminal contexts (i.e. pipes)
|
|
|
|
Minimal,
|
|
|
|
}
|
|
|
|
|
|
|
|
struct SpecialRender {
|
2024-05-09 18:09:14 +02:00
|
|
|
colors: bool,
|
|
|
|
|
2024-05-07 20:28:17 +02:00
|
|
|
dash: char,
|
|
|
|
bullet: char,
|
|
|
|
body_prefix: char,
|
|
|
|
|
|
|
|
red: &'static str,
|
|
|
|
bright_red: &'static str,
|
|
|
|
green: &'static str,
|
|
|
|
bright_green: &'static str,
|
|
|
|
blue: &'static str,
|
|
|
|
bright_blue: &'static str,
|
|
|
|
cyan: &'static str,
|
|
|
|
bright_cyan: &'static str,
|
|
|
|
yellow: &'static str,
|
|
|
|
bright_yellow: &'static str,
|
|
|
|
magenta: &'static str,
|
|
|
|
bright_magenta: &'static str,
|
|
|
|
black: &'static str,
|
|
|
|
dark_grey: &'static str,
|
|
|
|
light_grey: &'static str,
|
|
|
|
white: &'static str,
|
|
|
|
reset: &'static str,
|
2024-05-17 20:24:27 +02:00
|
|
|
|
|
|
|
hide_cursor: &'static str,
|
|
|
|
show_cursor: &'static str,
|
|
|
|
clear_line: &'static str,
|
2024-05-07 20:28:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
impl SpecialRender {
|
|
|
|
fn new(display: Style) -> Self {
|
|
|
|
let is_tty = std::io::stdout().is_terminal();
|
|
|
|
match display {
|
|
|
|
_ if !is_tty => Self::minimal(),
|
|
|
|
Style::Fancy => Self::fancy(),
|
|
|
|
Style::Minimal => Self::minimal(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn fancy() -> Self {
|
|
|
|
Self {
|
2024-05-09 18:09:14 +02:00
|
|
|
colors: true,
|
|
|
|
|
2024-05-07 20:28:17 +02:00
|
|
|
dash: '—',
|
|
|
|
bullet: '•',
|
|
|
|
body_prefix: '▌',
|
|
|
|
|
|
|
|
red: "\x1b[31m",
|
|
|
|
bright_red: "\x1b[91m",
|
|
|
|
green: "\x1b[32m",
|
|
|
|
bright_green: "\x1b[92m",
|
|
|
|
blue: "\x1b[34m",
|
|
|
|
bright_blue: "\x1b[94m",
|
|
|
|
cyan: "\x1b[36m",
|
|
|
|
bright_cyan: "\x1b[96m",
|
|
|
|
yellow: "\x1b[33m",
|
|
|
|
bright_yellow: "\x1b[93m",
|
|
|
|
magenta: "\x1b[35m",
|
|
|
|
bright_magenta: "\x1b[95m",
|
|
|
|
black: "\x1b[30m",
|
|
|
|
dark_grey: "\x1b[90m",
|
|
|
|
light_grey: "\x1b[37m",
|
|
|
|
white: "\x1b[97m",
|
|
|
|
reset: "\x1b[0m",
|
2024-05-17 20:24:27 +02:00
|
|
|
|
|
|
|
hide_cursor: "\x1b[?25l",
|
|
|
|
show_cursor: "\x1b[?25h",
|
|
|
|
clear_line: "\x1b[2K",
|
2024-05-07 20:28:17 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn minimal() -> Self {
|
|
|
|
Self {
|
2024-05-09 18:09:14 +02:00
|
|
|
colors: false,
|
|
|
|
|
2024-05-07 20:28:17 +02:00
|
|
|
dash: '-',
|
|
|
|
bullet: '-',
|
|
|
|
body_prefix: '>',
|
|
|
|
|
|
|
|
red: "",
|
|
|
|
bright_red: "",
|
|
|
|
green: "",
|
|
|
|
bright_green: "",
|
|
|
|
blue: "",
|
|
|
|
bright_blue: "",
|
|
|
|
cyan: "",
|
|
|
|
bright_cyan: "",
|
|
|
|
yellow: "",
|
|
|
|
bright_yellow: "",
|
|
|
|
magenta: "",
|
|
|
|
bright_magenta: "",
|
|
|
|
black: "",
|
|
|
|
dark_grey: "",
|
|
|
|
light_grey: "",
|
|
|
|
white: "",
|
|
|
|
reset: "",
|
2024-05-17 20:24:27 +02:00
|
|
|
|
|
|
|
hide_cursor: "",
|
|
|
|
show_cursor: "",
|
|
|
|
clear_line: "",
|
2024-05-07 20:28:17 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|