Initialize `radicle-term` crate
We split out all non-radicle terminal functionality so that it can be accessed by `radicle-node` without importing `radicle-cli`.
This commit is contained in:
parent
82d858314f
commit
02304875ce
|
|
@ -1835,6 +1835,7 @@ dependencies = [
|
||||||
"radicle-cob",
|
"radicle-cob",
|
||||||
"radicle-crypto",
|
"radicle-crypto",
|
||||||
"radicle-node",
|
"radicle-node",
|
||||||
|
"radicle-term",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_yaml",
|
"serde_yaml",
|
||||||
|
|
@ -2050,6 +2051,22 @@ dependencies = [
|
||||||
"thiserror",
|
"thiserror",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "radicle-term"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"concolor",
|
||||||
|
"inquire",
|
||||||
|
"once_cell",
|
||||||
|
"pretty_assertions",
|
||||||
|
"tempfile",
|
||||||
|
"termion 2.0.1",
|
||||||
|
"unicode-segmentation",
|
||||||
|
"unicode-width",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "radicle-tools"
|
name = "radicle-tools"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ default-members = [
|
||||||
"radicle-node",
|
"radicle-node",
|
||||||
"radicle-ssh",
|
"radicle-ssh",
|
||||||
"radicle-remote-helper",
|
"radicle-remote-helper",
|
||||||
|
"radicle-term",
|
||||||
]
|
]
|
||||||
|
|
||||||
[profile.container]
|
[profile.container]
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,10 @@ path = "../radicle-cob"
|
||||||
version = "0"
|
version = "0"
|
||||||
path = "../radicle-crypto"
|
path = "../radicle-crypto"
|
||||||
|
|
||||||
|
[dependencies.radicle-term]
|
||||||
|
version = "0"
|
||||||
|
path = "../radicle-term"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
pretty_assertions = { version = "1.3.0" }
|
pretty_assertions = { version = "1.3.0" }
|
||||||
radicle = { path = "../radicle", features = ["test"] }
|
radicle = { path = "../radicle", features = ["test"] }
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ use anyhow::anyhow;
|
||||||
|
|
||||||
use radicle::crypto::ssh;
|
use radicle::crypto::ssh;
|
||||||
use radicle::crypto::ssh::Passphrase;
|
use radicle::crypto::ssh::Passphrase;
|
||||||
|
use radicle::profile::env::RAD_PASSPHRASE;
|
||||||
use radicle::{profile, Profile};
|
use radicle::{profile, Profile};
|
||||||
|
|
||||||
use crate::terminal as term;
|
use crate::terminal as term;
|
||||||
|
|
@ -87,7 +88,7 @@ pub fn init(options: Options) -> anyhow::Result<()> {
|
||||||
let passphrase = if options.stdin {
|
let passphrase = if options.stdin {
|
||||||
term::passphrase_stdin()
|
term::passphrase_stdin()
|
||||||
} else {
|
} else {
|
||||||
term::passphrase_confirm("Enter a passphrase:")
|
term::passphrase_confirm("Enter a passphrase:", RAD_PASSPHRASE)
|
||||||
}?;
|
}?;
|
||||||
let spinner = term::spinner("Creating your Ed25519 keypair...");
|
let spinner = term::spinner("Creating your Ed25519 keypair...");
|
||||||
let profile = Profile::init(home, passphrase.clone())?;
|
let profile = Profile::init(home, passphrase.clone())?;
|
||||||
|
|
@ -132,7 +133,7 @@ pub fn authenticate(profile: &Profile, options: Options) -> anyhow::Result<()> {
|
||||||
let passphrase = if options.stdin {
|
let passphrase = if options.stdin {
|
||||||
term::passphrase_stdin()
|
term::passphrase_stdin()
|
||||||
} else {
|
} else {
|
||||||
term::passphrase()
|
term::passphrase(RAD_PASSPHRASE)
|
||||||
}?;
|
}?;
|
||||||
let spinner = term::spinner("Unlocking...");
|
let spinner = term::spinner("Unlocking...");
|
||||||
register(profile, passphrase)?;
|
register(profile, passphrase)?;
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,9 @@ use radicle::prelude::{Did, Doc};
|
||||||
use radicle::storage::ReadStorage as _;
|
use radicle::storage::ReadStorage as _;
|
||||||
use radicle_crypto::Verified;
|
use radicle_crypto::Verified;
|
||||||
|
|
||||||
|
use crate::terminal as term;
|
||||||
use crate::terminal::args::{Args, Error, Help};
|
use crate::terminal::args::{Args, Error, Help};
|
||||||
use crate::terminal::{self as term, Interactive};
|
use crate::terminal::Interactive;
|
||||||
|
|
||||||
pub const HELP: Help = Help {
|
pub const HELP: Help = Help {
|
||||||
name: "id",
|
name: "id",
|
||||||
|
|
|
||||||
|
|
@ -218,7 +218,7 @@ pub fn run(options: Options, ctx: impl term::Context) -> anyhow::Result<()> {
|
||||||
}
|
}
|
||||||
Operation::React { id, reaction } => {
|
Operation::React { id, reaction } => {
|
||||||
if let Ok(mut issue) = issues.get_mut(&id) {
|
if let Ok(mut issue) = issues.get_mut(&id) {
|
||||||
let (comment_id, _) = term::comment_select(&issue).unwrap();
|
let (comment_id, _) = term::io::comment_select(&issue).unwrap();
|
||||||
issue.react(*comment_id, reaction, &signer)?;
|
issue.react(*comment_id, reaction, &signer)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,18 @@
|
||||||
pub mod ansi;
|
|
||||||
pub mod args;
|
pub mod args;
|
||||||
pub mod cell;
|
pub use args::{Args, Error, Help};
|
||||||
pub mod cob;
|
pub mod cob;
|
||||||
pub mod command;
|
|
||||||
pub mod editor;
|
|
||||||
pub mod format;
|
pub mod format;
|
||||||
pub mod io;
|
pub mod io;
|
||||||
|
pub use io::{proposal, signer};
|
||||||
pub mod patch;
|
pub mod patch;
|
||||||
pub mod spinner;
|
pub use radicle_term::*;
|
||||||
pub mod table;
|
|
||||||
pub mod textbox;
|
|
||||||
|
|
||||||
use std::ffi::OsString;
|
use std::ffi::OsString;
|
||||||
use std::process;
|
use std::process;
|
||||||
|
|
||||||
use radicle::profile::Profile;
|
use radicle::profile::Profile;
|
||||||
|
|
||||||
pub use ansi::{paint, Paint};
|
use crate::terminal;
|
||||||
pub use args::{Args, Error, Help};
|
|
||||||
pub use editor::Editor;
|
|
||||||
pub use inquire::ui::Styled;
|
|
||||||
pub use io::*;
|
|
||||||
pub use spinner::{spinner, Spinner};
|
|
||||||
pub use table::Table;
|
|
||||||
pub use textbox::TextBox;
|
|
||||||
|
|
||||||
/// Context passed to all commands.
|
/// Context passed to all commands.
|
||||||
pub trait Context {
|
pub trait Context {
|
||||||
|
|
@ -117,7 +106,7 @@ where
|
||||||
match cmd.run(options, self::profile) {
|
match cmd.run(options, self::profile) {
|
||||||
Ok(()) => process::exit(0),
|
Ok(()) => process::exit(0),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
term::fail(&format!("{action} failed"), &err);
|
terminal::fail(&format!("{action} failed"), &err);
|
||||||
process::exit(1);
|
process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -136,47 +125,19 @@ pub fn profile() -> Result<Profile, anyhow::Error> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
|
pub fn fail(header: &str, error: &anyhow::Error) {
|
||||||
pub enum Interactive {
|
let err = error.to_string();
|
||||||
Yes,
|
let err = err.trim_end();
|
||||||
No,
|
let separator = if err.contains('\n') { ":\n" } else { ": " };
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Interactive {
|
println!(
|
||||||
fn default() -> Self {
|
"{ERROR_PREFIX} {}{}{error}",
|
||||||
Interactive::No
|
Paint::red(header).bold(),
|
||||||
|
Paint::red(separator),
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(Error::WithHint { hint, .. }) = error.downcast_ref::<Error>() {
|
||||||
|
println!("{} {}", ERROR_HINT_PREFIX, Paint::yellow(hint));
|
||||||
|
blank();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Interactive {
|
|
||||||
pub fn yes(&self) -> bool {
|
|
||||||
(*self).into()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn no(&self) -> bool {
|
|
||||||
!self.yes()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Interactive> for bool {
|
|
||||||
fn from(c: Interactive) -> Self {
|
|
||||||
match c {
|
|
||||||
Interactive::Yes => true,
|
|
||||||
Interactive::No => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<bool> for Interactive {
|
|
||||||
fn from(b: bool) -> Self {
|
|
||||||
if b {
|
|
||||||
Interactive::Yes
|
|
||||||
} else {
|
|
||||||
Interactive::No
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn style<T>(item: T) -> Paint<T> {
|
|
||||||
paint(item)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use std::{fmt, time};
|
use std::{fmt, time};
|
||||||
|
|
||||||
pub use crate::terminal::{style, Paint};
|
pub use radicle_term::format::*;
|
||||||
|
pub use radicle_term::{style, Paint};
|
||||||
|
|
||||||
use radicle::cob::{ObjectId, Timestamp};
|
use radicle::cob::{ObjectId, Timestamp};
|
||||||
use radicle::node::NodeId;
|
use radicle::node::NodeId;
|
||||||
|
|
@ -87,79 +88,3 @@ impl<'a> fmt::Display for Identity<'a> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn wrap<D: std::fmt::Display>(msg: D) -> Paint<D> {
|
|
||||||
Paint::wrapping(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn negative<D: std::fmt::Display>(msg: D) -> Paint<D> {
|
|
||||||
Paint::red(msg).bold()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn positive<D: std::fmt::Display>(msg: D) -> Paint<D> {
|
|
||||||
Paint::green(msg).bold()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn secondary<D: std::fmt::Display>(msg: D) -> Paint<D> {
|
|
||||||
Paint::blue(msg).bold()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tertiary<D: std::fmt::Display>(msg: D) -> Paint<D> {
|
|
||||||
Paint::cyan(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tertiary_bold<D: std::fmt::Display>(msg: D) -> Paint<D> {
|
|
||||||
Paint::cyan(msg).bold()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn yellow<D: std::fmt::Display>(msg: D) -> Paint<D> {
|
|
||||||
Paint::yellow(msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn highlight<D: std::fmt::Debug + std::fmt::Display>(input: D) -> Paint<D> {
|
|
||||||
Paint::green(input).bold()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn badge_primary<D: std::fmt::Display>(input: D) -> Paint<String> {
|
|
||||||
if Paint::is_enabled() {
|
|
||||||
Paint::magenta(format!(" {input} ")).invert()
|
|
||||||
} else {
|
|
||||||
Paint::new(format!("❲{input}❳"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn badge_positive<D: std::fmt::Display>(input: D) -> Paint<String> {
|
|
||||||
if Paint::is_enabled() {
|
|
||||||
Paint::green(format!(" {input} ")).invert()
|
|
||||||
} else {
|
|
||||||
Paint::new(format!("❲{input}❳"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn badge_negative<D: std::fmt::Display>(input: D) -> Paint<String> {
|
|
||||||
if Paint::is_enabled() {
|
|
||||||
Paint::red(format!(" {input} ")).invert()
|
|
||||||
} else {
|
|
||||||
Paint::new(format!("❲{input}❳"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn badge_secondary<D: std::fmt::Display>(input: D) -> Paint<String> {
|
|
||||||
if Paint::is_enabled() {
|
|
||||||
Paint::blue(format!(" {input} ")).invert()
|
|
||||||
} else {
|
|
||||||
Paint::new(format!("❲{input}❳"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn bold<D: std::fmt::Display>(input: D) -> Paint<D> {
|
|
||||||
Paint::white(input).bold()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn dim<D: std::fmt::Display>(input: D) -> Paint<D> {
|
|
||||||
Paint::new(input).dim()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn italic<D: std::fmt::Display>(input: D) -> Paint<D> {
|
|
||||||
Paint::new(input).italic().dim()
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,186 +1,19 @@
|
||||||
use std::fmt;
|
|
||||||
|
|
||||||
use inquire::ui::{ErrorMessageRenderConfig, StyleSheet, Styled};
|
|
||||||
use inquire::InquireError;
|
|
||||||
use inquire::{ui::Color, ui::RenderConfig, Confirm, CustomType, Password, Select};
|
|
||||||
use once_cell::sync::Lazy;
|
|
||||||
|
|
||||||
use radicle::cob::issue::Issue;
|
use radicle::cob::issue::Issue;
|
||||||
use radicle::cob::thread::{Comment, CommentId};
|
use radicle::cob::thread::{Comment, CommentId};
|
||||||
use radicle::crypto::ssh::keystore::{MemorySigner, Passphrase};
|
use radicle::crypto::ssh::keystore::MemorySigner;
|
||||||
use radicle::crypto::Signer;
|
use radicle::crypto::Signer;
|
||||||
use radicle::profile;
|
use radicle::profile::env::RAD_PASSPHRASE;
|
||||||
use radicle::profile::Profile;
|
use radicle::profile::Profile;
|
||||||
|
|
||||||
use super::command;
|
pub use radicle_term::io::*;
|
||||||
use super::format;
|
pub use radicle_term::spinner;
|
||||||
use super::spinner::spinner;
|
|
||||||
use super::Error;
|
|
||||||
use super::{style, Paint};
|
|
||||||
|
|
||||||
pub const ERROR_PREFIX: Paint<&str> = Paint::red("✗");
|
|
||||||
pub const ERROR_HINT_PREFIX: Paint<&str> = Paint::yellow("✗");
|
|
||||||
pub const WARNING_PREFIX: Paint<&str> = Paint::yellow("!");
|
|
||||||
pub const TAB: &str = " ";
|
|
||||||
|
|
||||||
/// Render configuration.
|
|
||||||
pub static CONFIG: Lazy<RenderConfig> = Lazy::new(|| RenderConfig {
|
|
||||||
prompt: StyleSheet::new().with_fg(Color::LightCyan),
|
|
||||||
prompt_prefix: Styled::new("?").with_fg(Color::LightBlue),
|
|
||||||
answered_prompt_prefix: Styled::new("✓").with_fg(Color::LightGreen),
|
|
||||||
answer: StyleSheet::new(),
|
|
||||||
highlighted_option_prefix: Styled::new("*").with_fg(Color::LightYellow),
|
|
||||||
help_message: StyleSheet::new().with_fg(Color::DarkGrey),
|
|
||||||
error_message: ErrorMessageRenderConfig::default_colored()
|
|
||||||
.with_prefix(Styled::new("✗").with_fg(Color::LightRed)),
|
|
||||||
..RenderConfig::default_colored()
|
|
||||||
});
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! info {
|
|
||||||
($($arg:tt)*) => ({
|
|
||||||
println!("{}", format_args!($($arg)*));
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! success {
|
|
||||||
($($arg:tt)*) => ({
|
|
||||||
$crate::terminal::io::success_args(format_args!($($arg)*));
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[macro_export]
|
|
||||||
macro_rules! tip {
|
|
||||||
($($arg:tt)*) => ({
|
|
||||||
$crate::terminal::io::tip_args(format_args!($($arg)*));
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub use info;
|
|
||||||
pub use success;
|
|
||||||
pub use tip;
|
|
||||||
|
|
||||||
pub fn success_args(args: fmt::Arguments) {
|
|
||||||
println!("{} {args}", Paint::green("✓"));
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn tip_args(args: fmt::Arguments) {
|
|
||||||
println!("👉 {}", style(format!("{args}")).italic());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn columns() -> Option<usize> {
|
|
||||||
termion::terminal_size().map(|(cols, _)| cols as usize).ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn headline(headline: impl fmt::Display) {
|
|
||||||
println!();
|
|
||||||
println!("{}", style(headline).bold());
|
|
||||||
println!();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn header(header: &str) {
|
|
||||||
println!();
|
|
||||||
println!("{}", style(format::yellow(header)).bold().underline());
|
|
||||||
println!();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn blob(text: impl fmt::Display) {
|
|
||||||
println!("{}", style(text.to_string().trim()).dim());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn blank() {
|
|
||||||
println!()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn print(msg: impl fmt::Display) {
|
|
||||||
println!("{msg}");
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn prefixed(prefix: &str, text: &str) -> String {
|
|
||||||
text.split('\n')
|
|
||||||
.map(|line| format!("{prefix}{line}\n"))
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn help(name: &str, version: &str, description: &str, usage: &str) {
|
|
||||||
println!("rad-{name} {version}\n{description}\n{usage}");
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn usage(name: &str, usage: &str) {
|
|
||||||
println!(
|
|
||||||
"{} {}\n{}",
|
|
||||||
ERROR_PREFIX,
|
|
||||||
Paint::red(format!("Error: rad-{name}: invalid usage")),
|
|
||||||
Paint::red(prefixed(TAB, usage)).dim()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn println(prefix: impl fmt::Display, msg: impl fmt::Display) {
|
|
||||||
println!("{prefix} {msg}");
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn indented(msg: impl fmt::Display) {
|
|
||||||
println!("{TAB}{msg}");
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn subcommand(msg: impl fmt::Display) {
|
|
||||||
println!("{} {}", style("$").dim(), style(msg).dim());
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn warning(warning: &str) {
|
|
||||||
println!(
|
|
||||||
"{} {} {warning}",
|
|
||||||
WARNING_PREFIX,
|
|
||||||
Paint::yellow("Warning:").bold(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn error(error: impl fmt::Display) {
|
|
||||||
println!("{ERROR_PREFIX} {error}");
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn fail(header: &str, error: &anyhow::Error) {
|
|
||||||
let err = error.to_string();
|
|
||||||
let err = err.trim_end();
|
|
||||||
let separator = if err.contains('\n') { ":\n" } else { ": " };
|
|
||||||
|
|
||||||
println!(
|
|
||||||
"{ERROR_PREFIX} {}{}{error}",
|
|
||||||
Paint::red(header).bold(),
|
|
||||||
Paint::red(separator),
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Some(Error::WithHint { hint, .. }) = error.downcast_ref::<Error>() {
|
|
||||||
println!("{} {}", ERROR_HINT_PREFIX, Paint::yellow(hint));
|
|
||||||
blank();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ask<D: fmt::Display>(prompt: D, default: bool) -> bool {
|
|
||||||
let prompt = prompt.to_string();
|
|
||||||
|
|
||||||
Confirm::new(&prompt)
|
|
||||||
.with_default(default)
|
|
||||||
.with_render_config(*CONFIG)
|
|
||||||
.prompt()
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn confirm<D: fmt::Display>(prompt: D) -> bool {
|
|
||||||
ask(prompt, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn abort<D: fmt::Display>(prompt: D) -> bool {
|
|
||||||
ask(prompt, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the signer. First we try getting it from ssh-agent, otherwise we prompt the user.
|
/// Get the signer. First we try getting it from ssh-agent, otherwise we prompt the user.
|
||||||
pub fn signer(profile: &Profile) -> anyhow::Result<Box<dyn Signer>> {
|
pub fn signer(profile: &Profile) -> anyhow::Result<Box<dyn Signer>> {
|
||||||
if let Ok(signer) = profile.signer() {
|
if let Ok(signer) = profile.signer() {
|
||||||
return Ok(signer);
|
return Ok(signer);
|
||||||
}
|
}
|
||||||
let passphrase = passphrase()?;
|
let passphrase = passphrase(RAD_PASSPHRASE)?;
|
||||||
let spinner = spinner("Unsealing key...");
|
let spinner = spinner("Unsealing key...");
|
||||||
let signer = MemorySigner::load(&profile.keystore, passphrase)?;
|
let signer = MemorySigner::load(&profile.keystore, passphrase)?;
|
||||||
|
|
||||||
|
|
@ -189,75 +22,6 @@ pub fn signer(profile: &Profile) -> anyhow::Result<Box<dyn Signer>> {
|
||||||
Ok(signer.boxed())
|
Ok(signer.boxed())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn input<S, E>(message: &str, default: Option<S>) -> anyhow::Result<S>
|
|
||||||
where
|
|
||||||
S: fmt::Display + std::str::FromStr<Err = E> + Clone,
|
|
||||||
E: fmt::Debug + fmt::Display,
|
|
||||||
{
|
|
||||||
let input = CustomType::<S>::new(message).with_render_config(*CONFIG);
|
|
||||||
let value = match default {
|
|
||||||
Some(default) => input.with_default(default).prompt()?,
|
|
||||||
None => input.prompt()?,
|
|
||||||
};
|
|
||||||
Ok(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn passphrase() -> Result<Passphrase, anyhow::Error> {
|
|
||||||
if let Some(p) = profile::env::passphrase() {
|
|
||||||
Ok(p)
|
|
||||||
} else {
|
|
||||||
Ok(Passphrase::from(
|
|
||||||
Password::new("Passphrase:")
|
|
||||||
.with_render_config(*CONFIG)
|
|
||||||
.with_display_mode(inquire::PasswordDisplayMode::Masked)
|
|
||||||
.without_confirmation()
|
|
||||||
.prompt()?,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn passphrase_confirm(prompt: &str) -> Result<Passphrase, anyhow::Error> {
|
|
||||||
if let Some(p) = profile::env::passphrase() {
|
|
||||||
Ok(p)
|
|
||||||
} else {
|
|
||||||
Ok(Passphrase::from(
|
|
||||||
Password::new(prompt)
|
|
||||||
.with_render_config(*CONFIG)
|
|
||||||
.with_display_mode(inquire::PasswordDisplayMode::Masked)
|
|
||||||
.with_custom_confirmation_message("Repeat passphrase:")
|
|
||||||
.with_custom_confirmation_error_message("The passphrases don't match.")
|
|
||||||
.with_help_message("This passphrase protects your radicle identity")
|
|
||||||
.prompt()?,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn passphrase_stdin() -> Result<Passphrase, anyhow::Error> {
|
|
||||||
let mut input = String::new();
|
|
||||||
std::io::stdin().read_line(&mut input)?;
|
|
||||||
|
|
||||||
Ok(Passphrase::from(input.trim_end().to_owned()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn select<'a, T>(
|
|
||||||
prompt: &str,
|
|
||||||
options: &'a [T],
|
|
||||||
active: &'a T,
|
|
||||||
) -> Result<Option<&'a T>, InquireError>
|
|
||||||
where
|
|
||||||
T: fmt::Display + Eq + PartialEq,
|
|
||||||
{
|
|
||||||
let active = options.iter().position(|o| o == active);
|
|
||||||
let selection =
|
|
||||||
Select::new(prompt, options.iter().collect::<Vec<_>>()).with_render_config(*CONFIG);
|
|
||||||
|
|
||||||
if let Some(active) = active {
|
|
||||||
selection.with_starting_cursor(active).prompt_skippable()
|
|
||||||
} else {
|
|
||||||
selection.prompt_skippable()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn comment_select(issue: &Issue) -> Option<(&CommentId, &Comment)> {
|
pub fn comment_select(issue: &Issue) -> Option<(&CommentId, &Comment)> {
|
||||||
let comments = issue.comments().collect::<Vec<_>>();
|
let comments = issue.comments().collect::<Vec<_>>();
|
||||||
let selection = Select::new(
|
let selection = Select::new(
|
||||||
|
|
@ -272,12 +36,6 @@ pub fn comment_select(issue: &Issue) -> Option<(&CommentId, &Comment)> {
|
||||||
comments.get(selection).copied()
|
comments.get(selection).copied()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn markdown(content: &str) {
|
|
||||||
if !content.is_empty() && command::bat(["-p", "-l", "md"], content).is_err() {
|
|
||||||
blob(content);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub mod proposal {
|
pub mod proposal {
|
||||||
use std::fmt::Write as _;
|
use std::fmt::Write as _;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
[package]
|
||||||
|
name = "radicle-term"
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = ["Alexis Sellier <alexis@radicle.xyz>"]
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow = { version = "1" }
|
||||||
|
concolor = { version = "0", features = ["api_unstable"] }
|
||||||
|
inquire = { version = "0.5.3", default-features = false, features = ["termion", "editor"] }
|
||||||
|
once_cell = { version = "1.13" }
|
||||||
|
termion = { version = "2" }
|
||||||
|
unicode-width = { version = "0.1.10", default-features = false }
|
||||||
|
unicode-segmentation = { version = "1.7.1" }
|
||||||
|
zeroize = { version = "1.1" }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
pretty_assertions = { version = "1.3.0" }
|
||||||
|
tempfile = { version = "3.3.0" }
|
||||||
|
|
@ -0,0 +1,97 @@
|
||||||
|
use std::ffi::OsString;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use anyhow::anyhow;
|
||||||
|
use radicle::crypto;
|
||||||
|
use radicle::prelude::{Did, Id, NodeId};
|
||||||
|
|
||||||
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
/// If this error is returned from argument parsing, help is displayed.
|
||||||
|
#[error("help invoked")]
|
||||||
|
Help,
|
||||||
|
/// If this error is returned from argument parsing, usage is displayed.
|
||||||
|
#[error("usage invoked")]
|
||||||
|
Usage,
|
||||||
|
/// An error with a hint.
|
||||||
|
#[error("{err}")]
|
||||||
|
WithHint {
|
||||||
|
err: anyhow::Error,
|
||||||
|
hint: &'static str,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Help {
|
||||||
|
pub name: &'static str,
|
||||||
|
pub description: &'static str,
|
||||||
|
pub version: &'static str,
|
||||||
|
pub usage: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Args: Sized {
|
||||||
|
fn from_env() -> anyhow::Result<Self> {
|
||||||
|
let args: Vec<_> = std::env::args_os().into_iter().skip(1).collect();
|
||||||
|
|
||||||
|
match Self::from_args(args) {
|
||||||
|
Ok((opts, unparsed)) => {
|
||||||
|
self::finish(unparsed)?;
|
||||||
|
|
||||||
|
Ok(opts)
|
||||||
|
}
|
||||||
|
Err(err) => Err(err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn from_args(args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_value<T: FromStr>(flag: &str, value: OsString) -> anyhow::Result<T>
|
||||||
|
where
|
||||||
|
<T as FromStr>::Err: std::error::Error,
|
||||||
|
{
|
||||||
|
value
|
||||||
|
.into_string()
|
||||||
|
.map_err(|_| anyhow!("the value specified for '--{}' is not valid unicode", flag))?
|
||||||
|
.parse()
|
||||||
|
.map_err(|e| anyhow!("invalid value specified for '--{}' ({})", flag, e))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn format(arg: lexopt::Arg) -> OsString {
|
||||||
|
match arg {
|
||||||
|
lexopt::Arg::Long(flag) => format!("--{flag}").into(),
|
||||||
|
lexopt::Arg::Short(flag) => format!("-{flag}").into(),
|
||||||
|
lexopt::Arg::Value(val) => val,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn finish(unparsed: Vec<OsString>) -> anyhow::Result<()> {
|
||||||
|
if let Some(arg) = unparsed.first() {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"unexpected argument `{}`",
|
||||||
|
arg.to_string_lossy()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn did(val: &OsString) -> anyhow::Result<Did> {
|
||||||
|
let val = val.to_string_lossy();
|
||||||
|
let Ok(peer) = Did::from_str(&val) else {
|
||||||
|
if crypto::PublicKey::from_str(&val).is_ok() {
|
||||||
|
return Err(anyhow!("expected DID, did you mean 'did:key:{val}'?"));
|
||||||
|
} else {
|
||||||
|
return Err(anyhow!("invalid DID '{}', expected 'did:key'", val));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(peer)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn nid(val: &OsString) -> anyhow::Result<NodeId> {
|
||||||
|
let val = val.to_string_lossy();
|
||||||
|
NodeId::from_str(&val).map_err(|_| anyhow!("invalid Node ID '{}'", val))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rid(val: &OsString) -> anyhow::Result<Id> {
|
||||||
|
let val = val.to_string_lossy();
|
||||||
|
Id::from_str(&val).map_err(|_| anyhow!("invalid Repository ID '{}'", val))
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use radicle::cob::patch;
|
||||||
|
|
||||||
|
use anyhow::anyhow;
|
||||||
|
|
||||||
|
pub fn parse_patch_id(val: OsString) -> Result<patch::PatchId, anyhow::Error> {
|
||||||
|
let val = val
|
||||||
|
.to_str()
|
||||||
|
.ok_or_else(|| anyhow!("patch id specified is not UTF-8"))?;
|
||||||
|
let patch_id =
|
||||||
|
patch::PatchId::from_str(val).map_err(|_| anyhow!("invalid patch id '{}'", val))?;
|
||||||
|
Ok(patch_id)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
use crate::Paint;
|
||||||
|
|
||||||
|
pub fn wrap<D: std::fmt::Display>(msg: D) -> Paint<D> {
|
||||||
|
Paint::wrapping(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn negative<D: std::fmt::Display>(msg: D) -> Paint<D> {
|
||||||
|
Paint::red(msg).bold()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn positive<D: std::fmt::Display>(msg: D) -> Paint<D> {
|
||||||
|
Paint::green(msg).bold()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn secondary<D: std::fmt::Display>(msg: D) -> Paint<D> {
|
||||||
|
Paint::blue(msg).bold()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tertiary<D: std::fmt::Display>(msg: D) -> Paint<D> {
|
||||||
|
Paint::cyan(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tertiary_bold<D: std::fmt::Display>(msg: D) -> Paint<D> {
|
||||||
|
Paint::cyan(msg).bold()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn yellow<D: std::fmt::Display>(msg: D) -> Paint<D> {
|
||||||
|
Paint::yellow(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn highlight<D: std::fmt::Debug + std::fmt::Display>(input: D) -> Paint<D> {
|
||||||
|
Paint::green(input).bold()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn badge_primary<D: std::fmt::Display>(input: D) -> Paint<String> {
|
||||||
|
if Paint::is_enabled() {
|
||||||
|
Paint::magenta(format!(" {input} ")).invert()
|
||||||
|
} else {
|
||||||
|
Paint::new(format!("❲{input}❳"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn badge_positive<D: std::fmt::Display>(input: D) -> Paint<String> {
|
||||||
|
if Paint::is_enabled() {
|
||||||
|
Paint::green(format!(" {input} ")).invert()
|
||||||
|
} else {
|
||||||
|
Paint::new(format!("❲{input}❳"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn badge_negative<D: std::fmt::Display>(input: D) -> Paint<String> {
|
||||||
|
if Paint::is_enabled() {
|
||||||
|
Paint::red(format!(" {input} ")).invert()
|
||||||
|
} else {
|
||||||
|
Paint::new(format!("❲{input}❳"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn badge_secondary<D: std::fmt::Display>(input: D) -> Paint<String> {
|
||||||
|
if Paint::is_enabled() {
|
||||||
|
Paint::blue(format!(" {input} ")).invert()
|
||||||
|
} else {
|
||||||
|
Paint::new(format!("❲{input}❳"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bold<D: std::fmt::Display>(input: D) -> Paint<D> {
|
||||||
|
Paint::white(input).bold()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dim<D: std::fmt::Display>(input: D) -> Paint<D> {
|
||||||
|
Paint::new(input).dim()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn italic<D: std::fmt::Display>(input: D) -> Paint<D> {
|
||||||
|
Paint::new(input).italic().dim()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,236 @@
|
||||||
|
use std::ffi::OsStr;
|
||||||
|
use std::{env, fmt};
|
||||||
|
|
||||||
|
use inquire::ui::{ErrorMessageRenderConfig, StyleSheet, Styled};
|
||||||
|
use inquire::InquireError;
|
||||||
|
use inquire::{ui::Color, ui::RenderConfig, Confirm, CustomType, Password};
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use zeroize::Zeroizing;
|
||||||
|
|
||||||
|
use crate::command;
|
||||||
|
use crate::format;
|
||||||
|
use crate::{style, Paint};
|
||||||
|
|
||||||
|
// TODO: Try not to export this.
|
||||||
|
pub use inquire::Select;
|
||||||
|
|
||||||
|
pub const ERROR_PREFIX: Paint<&str> = Paint::red("✗");
|
||||||
|
pub const ERROR_HINT_PREFIX: Paint<&str> = Paint::yellow("✗");
|
||||||
|
pub const WARNING_PREFIX: Paint<&str> = Paint::yellow("!");
|
||||||
|
pub const TAB: &str = " ";
|
||||||
|
|
||||||
|
/// Passphrase input.
|
||||||
|
pub type Passphrase = Zeroizing<String>;
|
||||||
|
|
||||||
|
/// Render configuration.
|
||||||
|
pub static CONFIG: Lazy<RenderConfig> = Lazy::new(|| RenderConfig {
|
||||||
|
prompt: StyleSheet::new().with_fg(Color::LightCyan),
|
||||||
|
prompt_prefix: Styled::new("?").with_fg(Color::LightBlue),
|
||||||
|
answered_prompt_prefix: Styled::new("✓").with_fg(Color::LightGreen),
|
||||||
|
answer: StyleSheet::new(),
|
||||||
|
highlighted_option_prefix: Styled::new("*").with_fg(Color::LightYellow),
|
||||||
|
help_message: StyleSheet::new().with_fg(Color::DarkGrey),
|
||||||
|
error_message: ErrorMessageRenderConfig::default_colored()
|
||||||
|
.with_prefix(Styled::new("✗").with_fg(Color::LightRed)),
|
||||||
|
..RenderConfig::default_colored()
|
||||||
|
});
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! info {
|
||||||
|
($($arg:tt)*) => ({
|
||||||
|
println!("{}", format_args!($($arg)*));
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! success {
|
||||||
|
($($arg:tt)*) => ({
|
||||||
|
$crate::io::success_args(format_args!($($arg)*));
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! tip {
|
||||||
|
($($arg:tt)*) => ({
|
||||||
|
$crate::io::tip_args(format_args!($($arg)*));
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub use info;
|
||||||
|
pub use success;
|
||||||
|
pub use tip;
|
||||||
|
|
||||||
|
pub fn success_args(args: fmt::Arguments) {
|
||||||
|
println!("{} {args}", Paint::green("✓"));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tip_args(args: fmt::Arguments) {
|
||||||
|
println!("👉 {}", style(format!("{args}")).italic());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn columns() -> Option<usize> {
|
||||||
|
termion::terminal_size().map(|(cols, _)| cols as usize).ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn headline(headline: impl fmt::Display) {
|
||||||
|
println!();
|
||||||
|
println!("{}", style(headline).bold());
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn header(header: &str) {
|
||||||
|
println!();
|
||||||
|
println!("{}", style(format::yellow(header)).bold().underline());
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn blob(text: impl fmt::Display) {
|
||||||
|
println!("{}", style(text.to_string().trim()).dim());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn blank() {
|
||||||
|
println!()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn print(msg: impl fmt::Display) {
|
||||||
|
println!("{msg}");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prefixed(prefix: &str, text: &str) -> String {
|
||||||
|
text.split('\n')
|
||||||
|
.map(|line| format!("{prefix}{line}\n"))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn help(name: &str, version: &str, description: &str, usage: &str) {
|
||||||
|
println!("rad-{name} {version}\n{description}\n{usage}");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn usage(name: &str, usage: &str) {
|
||||||
|
println!(
|
||||||
|
"{} {}\n{}",
|
||||||
|
ERROR_PREFIX,
|
||||||
|
Paint::red(format!("Error: rad-{name}: invalid usage")),
|
||||||
|
Paint::red(prefixed(TAB, usage)).dim()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn println(prefix: impl fmt::Display, msg: impl fmt::Display) {
|
||||||
|
println!("{prefix} {msg}");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn indented(msg: impl fmt::Display) {
|
||||||
|
println!("{TAB}{msg}");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn subcommand(msg: impl fmt::Display) {
|
||||||
|
println!("{} {}", style("$").dim(), style(msg).dim());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn warning(warning: &str) {
|
||||||
|
println!(
|
||||||
|
"{} {} {warning}",
|
||||||
|
WARNING_PREFIX,
|
||||||
|
Paint::yellow("Warning:").bold(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn error(error: impl fmt::Display) {
|
||||||
|
println!("{ERROR_PREFIX} {error}");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ask<D: fmt::Display>(prompt: D, default: bool) -> bool {
|
||||||
|
let prompt = prompt.to_string();
|
||||||
|
|
||||||
|
Confirm::new(&prompt)
|
||||||
|
.with_default(default)
|
||||||
|
.with_render_config(*CONFIG)
|
||||||
|
.prompt()
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn confirm<D: fmt::Display>(prompt: D) -> bool {
|
||||||
|
ask(prompt, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn abort<D: fmt::Display>(prompt: D) -> bool {
|
||||||
|
ask(prompt, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn input<S, E>(message: &str, default: Option<S>) -> anyhow::Result<S>
|
||||||
|
where
|
||||||
|
S: fmt::Display + std::str::FromStr<Err = E> + Clone,
|
||||||
|
E: fmt::Debug + fmt::Display,
|
||||||
|
{
|
||||||
|
let input = CustomType::<S>::new(message).with_render_config(*CONFIG);
|
||||||
|
let value = match default {
|
||||||
|
Some(default) => input.with_default(default).prompt()?,
|
||||||
|
None => input.prompt()?,
|
||||||
|
};
|
||||||
|
Ok(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn passphrase<K: AsRef<OsStr>>(var: K) -> Result<Passphrase, anyhow::Error> {
|
||||||
|
if let Ok(p) = env::var(var) {
|
||||||
|
Ok(Passphrase::from(p))
|
||||||
|
} else {
|
||||||
|
Ok(Passphrase::from(
|
||||||
|
Password::new("Passphrase:")
|
||||||
|
.with_render_config(*CONFIG)
|
||||||
|
.with_display_mode(inquire::PasswordDisplayMode::Masked)
|
||||||
|
.without_confirmation()
|
||||||
|
.prompt()?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn passphrase_confirm<K: AsRef<OsStr>>(
|
||||||
|
prompt: &str,
|
||||||
|
var: K,
|
||||||
|
) -> Result<Passphrase, anyhow::Error> {
|
||||||
|
if let Ok(p) = env::var(var) {
|
||||||
|
Ok(Passphrase::from(p))
|
||||||
|
} else {
|
||||||
|
Ok(Passphrase::from(
|
||||||
|
Password::new(prompt)
|
||||||
|
.with_render_config(*CONFIG)
|
||||||
|
.with_display_mode(inquire::PasswordDisplayMode::Masked)
|
||||||
|
.with_custom_confirmation_message("Repeat passphrase:")
|
||||||
|
.with_custom_confirmation_error_message("The passphrases don't match.")
|
||||||
|
.with_help_message("This passphrase protects your radicle identity")
|
||||||
|
.prompt()?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn passphrase_stdin() -> Result<Passphrase, anyhow::Error> {
|
||||||
|
let mut input = String::new();
|
||||||
|
std::io::stdin().read_line(&mut input)?;
|
||||||
|
|
||||||
|
Ok(Passphrase::from(input.trim_end().to_owned()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn select<'a, T>(
|
||||||
|
prompt: &str,
|
||||||
|
options: &'a [T],
|
||||||
|
active: &'a T,
|
||||||
|
) -> Result<Option<&'a T>, InquireError>
|
||||||
|
where
|
||||||
|
T: fmt::Display + Eq + PartialEq,
|
||||||
|
{
|
||||||
|
let active = options.iter().position(|o| o == active);
|
||||||
|
let selection =
|
||||||
|
Select::new(prompt, options.iter().collect::<Vec<_>>()).with_render_config(*CONFIG);
|
||||||
|
|
||||||
|
if let Some(active) = active {
|
||||||
|
selection.with_starting_cursor(active).prompt_skippable()
|
||||||
|
} else {
|
||||||
|
selection.prompt_skippable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn markdown(content: &str) {
|
||||||
|
if !content.is_empty() && command::bat(["-p", "-l", "md"], content).is_err() {
|
||||||
|
blob(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
pub mod ansi;
|
||||||
|
pub mod cell;
|
||||||
|
pub mod command;
|
||||||
|
pub mod editor;
|
||||||
|
pub mod format;
|
||||||
|
pub mod io;
|
||||||
|
pub mod spinner;
|
||||||
|
pub mod table;
|
||||||
|
pub mod textbox;
|
||||||
|
|
||||||
|
pub use ansi::{paint, Paint};
|
||||||
|
pub use editor::Editor;
|
||||||
|
pub use inquire::ui::Styled;
|
||||||
|
pub use io::*;
|
||||||
|
pub use spinner::{spinner, Spinner};
|
||||||
|
pub use table::Table;
|
||||||
|
pub use textbox::TextBox;
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
|
||||||
|
pub enum Interactive {
|
||||||
|
Yes,
|
||||||
|
No,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Interactive {
|
||||||
|
fn default() -> Self {
|
||||||
|
Interactive::No
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Interactive {
|
||||||
|
pub fn yes(&self) -> bool {
|
||||||
|
(*self).into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn no(&self) -> bool {
|
||||||
|
!self.yes()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Interactive> for bool {
|
||||||
|
fn from(c: Interactive) -> Self {
|
||||||
|
match c {
|
||||||
|
Interactive::Yes => true,
|
||||||
|
Interactive::No => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<bool> for Interactive {
|
||||||
|
fn from(b: bool) -> Self {
|
||||||
|
if b {
|
||||||
|
Interactive::Yes
|
||||||
|
} else {
|
||||||
|
Interactive::No
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn style<T>(item: T) -> Paint<T> {
|
||||||
|
paint(item)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
use radicle::git;
|
||||||
|
|
||||||
|
use crate::terminal as term;
|
||||||
|
use crate::terminal::cell::Cell as _;
|
||||||
|
|
||||||
|
/// The user supplied `Patch` description.
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
|
pub enum Message {
|
||||||
|
/// Prompt user to write comment in editor.
|
||||||
|
Edit,
|
||||||
|
/// Don't leave a comment.
|
||||||
|
Blank,
|
||||||
|
/// Use the following string as comment.
|
||||||
|
Text(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Message {
|
||||||
|
/// Get the `Message` as a string according to the method.
|
||||||
|
pub fn get(self, help: &str) -> String {
|
||||||
|
let comment = match self {
|
||||||
|
Message::Edit => term::Editor::new()
|
||||||
|
.extension("markdown")
|
||||||
|
.edit(help)
|
||||||
|
.ok()
|
||||||
|
.flatten(),
|
||||||
|
Message::Blank => None,
|
||||||
|
Message::Text(c) => Some(c),
|
||||||
|
};
|
||||||
|
let comment = comment.unwrap_or_default();
|
||||||
|
let comment = comment.trim();
|
||||||
|
|
||||||
|
comment.to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn append(&mut self, arg: &str) {
|
||||||
|
if let Message::Text(v) = self {
|
||||||
|
v.extend(["\n\n", arg]);
|
||||||
|
} else {
|
||||||
|
*self = Message::Text(arg.into());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Message {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::Edit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// List the given commits in a table.
|
||||||
|
pub fn list_commits(commits: &[git::raw::Commit]) -> anyhow::Result<()> {
|
||||||
|
let mut table = term::Table::default();
|
||||||
|
|
||||||
|
for commit in commits {
|
||||||
|
let message = commit
|
||||||
|
.summary_bytes()
|
||||||
|
.unwrap_or_else(|| commit.message_bytes());
|
||||||
|
table.push([
|
||||||
|
term::format::secondary(term::format::oid(commit.id())),
|
||||||
|
term::format::italic(String::from_utf8_lossy(message).to_string()),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
table.render();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Print commits ahead and behind.
|
||||||
|
pub fn print_commits_ahead_behind(
|
||||||
|
repo: &git::raw::Repository,
|
||||||
|
left: git::raw::Oid,
|
||||||
|
right: git::raw::Oid,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let (ahead, behind) = repo.graph_ahead_behind(left, right)?;
|
||||||
|
|
||||||
|
term::info!(
|
||||||
|
"{} commit(s) ahead, {} commit(s) behind",
|
||||||
|
term::format::positive(ahead),
|
||||||
|
if behind > 0 {
|
||||||
|
term::format::negative(behind)
|
||||||
|
} else {
|
||||||
|
term::format::dim(behind)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Print title and description in a text box.
|
||||||
|
pub fn print_title_desc(title: &str, description: &str) {
|
||||||
|
let title_pretty = &term::format::dim(format!("╭─ {title} ───────"));
|
||||||
|
term::print(title_pretty);
|
||||||
|
term::blank();
|
||||||
|
|
||||||
|
if description.is_empty() {
|
||||||
|
term::print(term::format::italic("No description provided."));
|
||||||
|
} else {
|
||||||
|
term::markdown(description);
|
||||||
|
}
|
||||||
|
|
||||||
|
term::blank();
|
||||||
|
term::print(term::format::dim(format!(
|
||||||
|
"╰{}",
|
||||||
|
"─".repeat(title_pretty.to_string().width() - 1)
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
|
@ -3,8 +3,8 @@ use std::mem::ManuallyDrop;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::{fmt, io, thread, time};
|
use std::{fmt, io, thread, time};
|
||||||
|
|
||||||
use crate::terminal::io::{ERROR_PREFIX, WARNING_PREFIX};
|
use crate::io::{ERROR_PREFIX, WARNING_PREFIX};
|
||||||
use crate::terminal::Paint;
|
use crate::Paint;
|
||||||
|
|
||||||
/// How much time to wait between spinner animation updates.
|
/// How much time to wait between spinner animation updates.
|
||||||
pub const DEFAULT_TICK: time::Duration = time::Duration::from_millis(99);
|
pub const DEFAULT_TICK: time::Duration = time::Duration::from_millis(99);
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
//!
|
//!
|
||||||
//! Example:
|
//! Example:
|
||||||
//! ```
|
//! ```
|
||||||
//! use radicle_cli::terminal::table::*;
|
//! use radicle_term::table::*;
|
||||||
//!
|
//!
|
||||||
//! let mut t = Table::new(TableOptions::default());
|
//! let mut t = Table::new(TableOptions::default());
|
||||||
//! t.push(["pest", "biological control"]);
|
//! t.push(["pest", "biological control"]);
|
||||||
|
|
@ -18,8 +18,8 @@
|
||||||
//! ```
|
//! ```
|
||||||
use std::io;
|
use std::io;
|
||||||
|
|
||||||
use crate::terminal as term;
|
use crate as term;
|
||||||
use crate::terminal::cell::Cell;
|
use crate::cell::Cell;
|
||||||
|
|
||||||
/// Used to specify maximum width or height.
|
/// Used to specify maximum width or height.
|
||||||
#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)]
|
#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)]
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
use crate::terminal as term;
|
use crate as term;
|
||||||
use crate::terminal::cell::Cell as _;
|
use crate::cell::Cell as _;
|
||||||
|
|
||||||
pub struct TextBox {
|
pub struct TextBox {
|
||||||
pub body: String,
|
pub body: String,
|
||||||
Loading…
Reference in New Issue