summaryrefslogtreecommitdiffstats
path: root/src/main.rs
blob: c7674d0f5eafa24ccdc117ce6aa2bb830643f1d2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
use clap::{Parser, Subcommand};
use eyre::eyre;
use tokio::io::AsyncWriteExt;
use url::Url;

mod keys;
use keys::*;

mod auth;
mod issues;
mod repo;

#[derive(Parser, Debug)]
pub struct App {
    #[clap(subcommand)]
    command: Command,
}

#[derive(Subcommand, Clone, Debug)]
pub enum Command {
    #[clap(subcommand)]
    Repo(repo::RepoCommand),
    #[clap(subcommand)]
    Issue(issues::IssueCommand),
    User {
        #[clap(long, short)]
        host: Option<String>,
    },
    #[clap(subcommand)]
    Auth(auth::AuthCommand),
}

#[tokio::main]
async fn main() -> eyre::Result<()> {
    let args = App::parse();
    let mut keys = KeyInfo::load().await?;

    match args.command {
        Command::Repo(subcommand) => subcommand.run(&keys).await?,
        Command::Issue(subcommand) => subcommand.run(&keys).await?,
        Command::User { host } => {
            let host = host.map(|host| Url::parse(&host)).transpose()?;
            let url = match host {
                Some(url) => url,
                None => repo::RepoInfo::get_current()?.url().clone(),
            };
            let name = keys.get_login(&url)?.username();
            eprintln!("currently signed in to {name}@{url}");
        }
        Command::Auth(subcommand) => subcommand.run(&mut keys).await?,
    }

    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?
}

async fn editor(contents: &mut String, ext: Option<&str>) -> eyre::Result<()> {
    let editor = std::env::var_os("EDITOR").ok_or_else(|| eyre!("unable to locate editor"))?;

    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");
        let status = tokio::process::Command::new(editor)
            .arg(&path)
            .status()
            .await?;
        if !status.success() {
            eyre::bail!("editor exited unsuccessfully");
        }

        *contents = tokio::fs::read_to_string(&path).await?;
        eprint!("                 \r");

        Ok(())
    })()
    .await;

    tokio::fs::remove_file(path).await?;
    res?;
    Ok(())
}

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))
}