patch: Initialize w/ unified command

This commit is contained in:
Erik Kundt
2023-12-23 11:41:46 +01:00
parent bfc6909cfa
commit 74940ca7bf
15 changed files with 271 additions and 569 deletions
+1 -1
View File
@@ -7,7 +7,7 @@ edition = "2021"
build = "build.rs"
[[bin]]
name = "radicle-tui"
name = "rad-tui"
path = "bin/main.rs"
[dependencies]
+4
View File
@@ -0,0 +1,4 @@
#[path = "commands/help.rs"]
pub mod tui_help;
#[path = "commands/patch.rs"]
pub mod tui_patch;
+68
View File
@@ -0,0 +1,68 @@
use std::ffi::OsString;
use radicle_term as term;
use crate::terminal::args::{Args, Error, Help};
use crate::terminal::Context;
use super::*;
pub const HELP: Help = Help {
name: "help",
description: "TUI help",
version: env!("CARGO_PKG_VERSION"),
usage: "Usage: rad-tui help [--help]",
};
const COMMANDS: &[Help] = &[tui_help::HELP, tui_patch::HELP];
#[derive(Default)]
pub struct Options {}
impl Args for Options {
fn from_args(args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> {
let mut parser = lexopt::Parser::from_args(args);
if let Some(arg) = parser.next()? {
return Err(anyhow::anyhow!(arg.unexpected()));
}
Err(Error::HelpManual { name: "rad-tui" }.into())
}
}
pub fn run(_options: Options, ctx: impl Context) -> anyhow::Result<()> {
term::print("Usage: rad-tui <command> [--help]");
if let Err(e) = ctx.profile() {
term::blank();
match e.downcast_ref() {
Some(Error::WithHint { err, hint }) => {
term::print(term::format::yellow(err));
term::print(term::format::yellow(hint));
}
Some(e) => {
term::error(e);
}
None => {
term::error(e);
}
}
term::blank();
}
term::print("Common `rad-tui` commands used in various situations:");
term::blank();
for help in COMMANDS {
term::info!(
"\t{} {}",
term::format::bold(format!("{:-12}", help.name)),
term::format::dim(help.description)
);
}
term::blank();
term::print("See `rad-tui <command> --help` to learn about a specific command.");
term::blank();
Ok(())
}
+44 -374
View File
@@ -1,397 +1,67 @@
pub mod event;
pub mod page;
pub mod subscription;
#[path = "patch/suite.rs"]
mod suite;
use anyhow::Result;
use std::ffi::OsString;
use radicle::cob::issue::IssueId;
use radicle::cob::patch::PatchId;
use radicle::identity::{Id, Project};
use radicle::prelude::Signer;
use radicle::profile::Profile;
use anyhow::anyhow;
use radicle_tui::ui::widget;
use tuirealm::application::PollStrategy;
use tuirealm::{Application, Frame, NoUserEvent, Sub, SubClause};
use crate::terminal;
use crate::terminal::args::{Args, Error, Help};
use radicle_tui::ui::context::Context;
use radicle_tui::ui::theme::{self, Theme};
use radicle_tui::Tui;
use radicle_tui::{cob, ui};
pub const HELP: Help = Help {
name: "patch",
description: "Terminal interfaces for patches",
version: env!("CARGO_PKG_VERSION"),
usage: r#"
Usage
use page::{HomeView, PatchView};
rad-tui patch
use self::page::{IssuePage, PageStack};
General options
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
pub enum HomeCid {
Header,
Dashboard,
IssueBrowser,
PatchBrowser,
Context,
Shortcuts,
}
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
pub enum PatchCid {
Header,
Activity,
Files,
Context,
Shortcuts,
}
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
pub enum IssueCid {
Header,
List,
Details,
Context,
Form,
Shortcuts,
}
/// All component ids known to this application.
#[derive(Debug, Eq, PartialEq, Clone, Hash)]
pub enum Cid {
Home(HomeCid),
Issue(IssueCid),
Patch(PatchCid),
GlobalListener,
Popup,
}
/// Messages handled by this application.
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum HomeMessage {
RefreshIssues(Option<IssueId>),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum IssueCobMessage {
Create {
title: String,
tags: String,
assignees: String,
description: String,
},
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum IssueMessage {
Show(Option<IssueId>),
Changed(IssueId),
Focus(IssueCid),
Created(IssueId),
Cob(IssueCobMessage),
OpenForm,
HideForm,
Leave(Option<IssueId>),
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PatchMessage {
Show(PatchId),
Leave,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum PopupMessage {
Info(String),
Warning(String),
Error(String),
Hide,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Message {
Home(HomeMessage),
Issue(IssueMessage),
Patch(PatchMessage),
NavigationChanged(u16),
FormSubmitted(String),
Popup(PopupMessage),
Tick,
Quit,
Batch(Vec<Message>),
}
--help Print help
"#,
};
#[allow(dead_code)]
pub struct App {
context: Context,
pages: PageStack,
theme: Theme,
quit: bool,
pub struct Options {
op: Operation,
}
/// Creates a new application using a tui-realm-application, mounts all
/// components and sets focus to a default one.
impl App {
pub fn new(profile: Profile, id: Id, project: Project, signer: Box<dyn Signer>) -> Self {
Self {
context: Context::new(profile, id, project, signer),
pages: PageStack::default(),
theme: theme::default_dark(),
quit: false,
}
}
pub enum Operation {
Suite,
}
fn view_home(
&mut self,
app: &mut Application<Cid, Message, NoUserEvent>,
theme: &Theme,
) -> Result<()> {
let home = Box::new(HomeView::new(theme.clone()));
self.pages.push(home, app, &self.context, theme)?;
#[derive(Default, PartialEq, Eq)]
pub enum OperationName {
#[default]
Suite,
}
Ok(())
}
impl Args for Options {
fn from_args(args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)> {
use lexopt::prelude::*;
fn view_patch(
&mut self,
app: &mut Application<Cid, Message, NoUserEvent>,
id: PatchId,
theme: &Theme,
) -> Result<()> {
let repo = self.context.repository();
let mut parser = lexopt::Parser::from_args(args);
let op: Option<OperationName> = None;
if let Some(patch) = cob::patch::find(repo, &id)? {
let view = Box::new(PatchView::new(theme.clone(), (id, patch)));
self.pages.push(view, app, &self.context, theme)?;
Ok(())
} else {
Err(anyhow::anyhow!(
"Could not mount 'page::PatchView'. Patch not found."
))
}
}
fn view_issue(
&mut self,
app: &mut Application<Cid, Message, NoUserEvent>,
id: Option<IssueId>,
theme: &Theme,
) -> Result<()> {
let repo = self.context.repository();
match id {
Some(id) => {
if let Some(issue) = cob::issue::find(repo, &id)? {
let view = Box::new(IssuePage::new(&self.context, theme, Some((id, issue))));
self.pages.push(view, app, &self.context, theme)?;
Ok(())
} else {
Err(anyhow::anyhow!(
"Could not mount 'page::IssueView'. Issue not found."
))
#[allow(clippy::never_loop)]
while let Some(arg) = parser.next()? {
match arg {
Long("help") | Short('h') => {
return Err(Error::Help.into());
}
}
None => {
let view = Box::new(IssuePage::new(&self.context, theme, None));
self.pages.push(view, app, &self.context, theme)?;
Ok(())
_ => return Err(anyhow!(arg.unexpected())),
}
}
}
fn process(
&mut self,
app: &mut Application<Cid, Message, NoUserEvent>,
message: Message,
) -> Result<Option<Message>> {
let theme = theme::default_dark();
match message {
Message::Batch(messages) => {
let mut results = vec![];
for message in messages {
if let Some(result) = self.process(app, message)? {
results.push(result);
}
}
match results.len() {
0 => Ok(None),
1 => Ok(Some(results[0].to_owned())),
_ => Ok(Some(Message::Batch(results))),
}
}
Message::Issue(IssueMessage::Cob(IssueCobMessage::Create {
title,
tags,
assignees,
description,
})) => match self.create_issue(title, description, tags, assignees) {
Ok(id) => {
self.context.reload();
Ok(Some(Message::Batch(vec![
Message::Issue(IssueMessage::HideForm),
Message::Issue(IssueMessage::Created(id)),
])))
}
Err(err) => {
let error = format!("{:?}", err);
self.show_error_popup(app, &theme, &error)?;
Ok(None)
}
},
Message::Issue(IssueMessage::Show(id)) => {
self.view_issue(app, id, &theme)?;
Ok(None)
}
Message::Issue(IssueMessage::Leave(id)) => {
self.pages.pop(app)?;
Ok(Some(Message::Home(HomeMessage::RefreshIssues(id))))
}
Message::Patch(PatchMessage::Show(id)) => {
self.view_patch(app, id, &theme)?;
Ok(None)
}
Message::Patch(PatchMessage::Leave) => {
self.pages.pop(app)?;
Ok(None)
}
Message::Popup(PopupMessage::Info(info)) => {
self.show_info_popup(app, &theme, &info)?;
Ok(None)
}
Message::Popup(PopupMessage::Warning(warning)) => {
self.show_warning_popup(app, &theme, &warning)?;
Ok(None)
}
Message::Popup(PopupMessage::Error(error)) => {
self.show_error_popup(app, &theme, &error)?;
Ok(None)
}
Message::Popup(PopupMessage::Hide) => {
self.hide_popup(app)?;
Ok(None)
}
Message::Quit => {
self.quit = true;
Ok(None)
}
_ => self
.pages
.peek_mut()?
.update(app, &self.context, &theme, message),
}
}
fn show_info_popup(
&mut self,
app: &mut Application<Cid, Message, NoUserEvent>,
theme: &Theme,
message: &str,
) -> Result<()> {
let popup = widget::common::info(theme, message);
app.remount(Cid::Popup, popup.to_boxed(), vec![])?;
app.active(&Cid::Popup)?;
Ok(())
}
fn show_warning_popup(
&mut self,
app: &mut Application<Cid, Message, NoUserEvent>,
theme: &Theme,
message: &str,
) -> Result<()> {
let popup = widget::common::warning(theme, message);
app.remount(Cid::Popup, popup.to_boxed(), vec![])?;
app.active(&Cid::Popup)?;
Ok(())
}
fn show_error_popup(
&mut self,
app: &mut Application<Cid, Message, NoUserEvent>,
theme: &Theme,
message: &str,
) -> Result<()> {
let popup = widget::common::error(theme, message);
app.remount(Cid::Popup, popup.to_boxed(), vec![])?;
app.active(&Cid::Popup)?;
Ok(())
}
fn hide_popup(&mut self, app: &mut Application<Cid, Message, NoUserEvent>) -> Result<()> {
app.blur()?;
app.umount(&Cid::Popup)?;
Ok(())
}
fn create_issue(
&mut self,
title: String,
description: String,
labels: String,
assignees: String,
) -> Result<IssueId> {
let repository = self.context.repository();
let signer = self.context.signer();
let labels = cob::parse_labels(labels)?;
let assignees = cob::parse_assignees(assignees)?;
cob::issue::create(
repository,
signer,
title,
description,
labels.as_slice(),
assignees.as_slice(),
)
let op = match op.unwrap_or_default() {
OperationName::Suite => Operation::Suite,
};
Ok((Options { op }, vec![]))
}
}
impl Tui<Cid, Message> for App {
fn init(&mut self, app: &mut Application<Cid, Message, NoUserEvent>) -> Result<()> {
self.view_home(app, &self.theme.clone())?;
// Add global key listener and subscribe to key events
let global = ui::widget::common::global_listener().to_boxed();
app.mount(
Cid::GlobalListener,
global,
vec![Sub::new(subscription::global_clause(), SubClause::Always)],
)?;
Ok(())
}
fn view(&mut self, app: &mut Application<Cid, Message, NoUserEvent>, frame: &mut Frame) {
if let Ok(page) = self.pages.peek_mut() {
page.view(app, frame);
}
if app.mounted(&Cid::Popup) {
app.view(&Cid::Popup, frame, frame.size());
}
}
fn update(&mut self, app: &mut Application<Cid, Message, NoUserEvent>) -> Result<bool> {
match app.tick(PollStrategy::Once) {
Ok(messages) if !messages.is_empty() => {
for message in messages {
let mut msg = Some(message);
while msg.is_some() {
msg = self.process(app, msg.unwrap())?;
}
}
Ok(true)
}
_ => Ok(false),
}
}
fn quit(&self) -> bool {
self.quit
}
pub fn run(_options: Options, _ctx: impl terminal::Context) -> anyhow::Result<()> {
Ok(())
}
-87
View File
@@ -1,87 +0,0 @@
mod app;
use std::process;
use anyhow::anyhow;
use radicle::profile;
use log::info;
use log::LevelFilter;
use radicle_term as term;
use radicle_tui as tui;
use tui::context;
use tui::Window;
pub const NAME: &str = "rad-patch-tui";
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
pub const GIT_HEAD: &str = env!("GIT_HEAD");
pub const FPS: u64 = 60;
pub const HELP: &str = r#"
Usage
rad-patch-tui [<option>...]
Options
--version Print version
--help Print help
"#;
struct Options;
impl Options {
#[allow(clippy::never_loop)]
fn from_env() -> Result<Self, anyhow::Error> {
use lexopt::prelude::*;
let mut parser = lexopt::Parser::from_env();
while let Some(arg) = parser.next()? {
match arg {
Long("version") => {
println!("{NAME} {VERSION}+{GIT_HEAD}");
process::exit(0);
}
Long("help") | Short('h') => {
println!("{HELP}");
process::exit(0);
}
_ => anyhow::bail!(arg.unexpected()),
}
}
Ok(Self {})
}
}
fn execute() -> anyhow::Result<()> {
let _ = Options::from_env()?;
let (_, id) = radicle::rad::cwd()
.map_err(|_| anyhow!("this command must be run in the context of a project"))?;
let context = context::Context::new(id)?;
let logfile = format!(
"{}/rad-patch-tui.log",
profile::home()?.path().to_string_lossy()
);
simple_logging::log_to_file(logfile, LevelFilter::Info)?;
info!("Launching window...");
let mut window = Window::default();
window.run(&mut app::App::new(context), 1000 / FPS)?;
Ok(())
}
fn main() {
if let Err(err) = execute() {
term::error(format!("Error: rad-patch-tui: {err}"));
process::exit(1);
}
}
@@ -1,5 +1,8 @@
#[path = "suite/event.rs"]
mod event;
#[path = "suite/page.rs"]
mod page;
#[path = "suite/ui.rs"]
mod ui;
use std::hash::Hash;
@@ -85,6 +88,7 @@ pub struct App {
/// Creates a new application using a tui-realm-application, mounts all
/// components and sets focus to a default one.
#[allow(dead_code)]
impl App {
pub fn new(context: Context) -> Self {
Self {
+112 -4
View File
@@ -1,7 +1,115 @@
// use radicle::profile;
mod commands;
mod terminal;
fn main() {
use std::ffi::OsString;
use std::io;
use std::{iter, process};
}
use anyhow::anyhow;
use radicle::version;
use radicle_term as term;
use commands::*;
pub const NAME: &str = "rad-tui";
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
pub const DESCRIPTION: &str = "Radicle terminal interfaces";
pub const GIT_HEAD: &str = env!("GIT_HEAD");
#[derive(Debug)]
enum Command {
Other(Vec<OsString>),
Help,
Version,
}
fn main() {
match parse_args().map_err(Some).and_then(run) {
Ok(_) => process::exit(0),
Err(err) => {
if let Some(err) = err {
term::error(format!("rad: {err}"));
}
process::exit(1);
}
}
}
fn parse_args() -> anyhow::Result<Command> {
use lexopt::prelude::*;
let mut parser = lexopt::Parser::from_env();
let mut command = None;
while let Some(arg) = parser.next()? {
match arg {
Long("help") | Short('h') => {
command = Some(Command::Help);
}
Long("version") => {
command = Some(Command::Version);
}
Value(val) if command.is_none() => {
if val == *"." {
command = Some(Command::Other(vec![OsString::from("inspect")]));
} else {
let args = iter::once(val)
.chain(iter::from_fn(|| parser.value().ok()))
.collect();
command = Some(Command::Other(args))
}
}
_ => return Err(anyhow::anyhow!(arg.unexpected())),
}
}
Ok(command.unwrap_or_else(|| Command::Other(vec![])))
}
fn print_help() -> anyhow::Result<()> {
version::print(&mut io::stdout(), NAME, VERSION, GIT_HEAD)?;
println!("{DESCRIPTION}");
println!();
tui_help::run(Default::default(), terminal::profile)
}
fn run(command: Command) -> Result<(), Option<anyhow::Error>> {
match command {
Command::Version => {
version::print(&mut io::stdout(), NAME, VERSION, GIT_HEAD)
.map_err(|e| Some(e.into()))?;
}
Command::Help => {
print_help()?;
}
Command::Other(args) => {
let exe = args.first();
if let Some(Some(exe)) = exe.map(|s| s.to_str()) {
run_other(exe, &args[1..])?;
} else {
print_help()?;
}
}
}
Ok(())
}
fn run_other(exe: &str, args: &[OsString]) -> Result<(), Option<anyhow::Error>> {
match exe {
"patch" => {
terminal::run_command_args::<tui_patch::Options, _>(
tui_patch::HELP,
tui_patch::run,
args.to_vec(),
);
}
other => Err(Some(anyhow!(
"`{other}` is not a command. See `rad-tui --help` for a list of commands.",
))),
}
}
+12 -11
View File
@@ -1,10 +1,11 @@
pub mod args;
pub use args::{Args, Error, Help};
pub mod io;
use std::ffi::OsString;
use std::process;
pub use args::{Args, Error, Help};
use radicle_term as term;
use radicle::profile::Profile;
@@ -45,15 +46,15 @@ where
}
}
pub fn run_command<A, C>(help: Help, cmd: C) -> !
where
A: Args,
C: Command<A, fn() -> anyhow::Result<Profile>>,
{
let args = std::env::args_os().skip(1).collect();
// pub fn run_command<A, C>(help: Help, cmd: C) -> !
// where
// A: Args,
// C: Command<A, fn() -> anyhow::Result<Profile>>,
// {
// let args = std::env::args_os().skip(1).collect();
run_command_args(help, cmd, args)
}
// run_command_args(help, cmd, args)
// }
pub fn run_command_args<A, C>(help: Help, cmd: C, args: Vec<OsString>) -> !
where
@@ -91,7 +92,7 @@ where
term::error(format!("rad {}: {err}", help.name));
if let Some(hint) = hint {
term::hint(hint);
term::hint(hint);
}
process::exit(1);
}
@@ -129,6 +130,6 @@ pub fn fail(_name: &str, error: &anyhow::Error) {
}
if let Some(Error::WithHint { hint, .. }) = error.downcast_ref::<Error>() {
term::hint(hint);
term::hint(hint);
}
}
+8 -92
View File
@@ -1,14 +1,9 @@
use std::ffi::OsString;
use std::str::FromStr;
use std::time;
use anyhow::anyhow;
use radicle::cob::{self, issue, patch};
use radicle::crypto;
use radicle::git::RefString;
use radicle::node::{Address, Alias};
use radicle::prelude::{Did, Id, NodeId};
use radicle::cob::{issue, patch};
/// Git revision parameter. Supports extended SHA-1 syntax.
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -21,6 +16,7 @@ impl From<String> for Rev {
}
#[derive(thiserror::Error, Debug)]
#[allow(dead_code)]
pub enum Error {
/// If this error is returned from argument parsing, help is displayed.
#[error("help invoked")]
@@ -63,6 +59,7 @@ pub trait Args: Sized {
fn from_args(args: Vec<OsString>) -> anyhow::Result<(Self, Vec<OsString>)>;
}
#[allow(dead_code)]
pub fn parse_value<T: FromStr>(flag: &str, value: OsString) -> anyhow::Result<T>
where
<T as FromStr>::Err: std::error::Error,
@@ -74,6 +71,7 @@ where
.map_err(|e| anyhow!("invalid value specified for '--{}' ({})", flag, e))
}
#[allow(dead_code)]
pub fn format(arg: lexopt::Arg) -> OsString {
match arg {
lexopt::Arg::Long(flag) => format!("--{flag}").into(),
@@ -82,6 +80,7 @@ pub fn format(arg: lexopt::Arg) -> OsString {
}
}
#[allow(dead_code)]
pub fn finish(unparsed: Vec<OsString>) -> anyhow::Result<()> {
if let Some(arg) = unparsed.first() {
return Err(anyhow::anyhow!(
@@ -92,103 +91,20 @@ pub fn finish(unparsed: Vec<OsString>) -> anyhow::Result<()> {
Ok(())
}
pub fn refstring(flag: &str, value: OsString) -> anyhow::Result<RefString> {
RefString::try_from(
value
.into_string()
.map_err(|_| anyhow!("the value specified for '--{}' is not valid UTF-8", flag))?,
)
.map_err(|_| {
anyhow!(
"the value specified for '--{}' is not a valid ref string",
flag
)
})
}
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))
}
pub fn pubkey(val: &OsString) -> anyhow::Result<NodeId> {
let Ok(did) = did(val) else {
let nid = nid(val)?;
return Ok(nid);
};
Ok(did.as_key().to_owned())
}
pub fn addr(val: &OsString) -> anyhow::Result<Address> {
let val = val.to_string_lossy();
Address::from_str(&val).map_err(|_| anyhow!("invalid address '{}'", val))
}
pub fn number(val: &OsString) -> anyhow::Result<usize> {
let val = val.to_string_lossy();
usize::from_str(&val).map_err(|_| anyhow!("invalid number '{}'", val))
}
pub fn seconds(val: &OsString) -> anyhow::Result<time::Duration> {
let val = val.to_string_lossy();
let secs = u64::from_str(&val).map_err(|_| anyhow!("invalid number of seconds '{}'", val))?;
Ok(time::Duration::from_secs(secs))
}
pub fn string(val: &OsString) -> String {
val.to_string_lossy().to_string()
}
#[allow(dead_code)]
pub fn rev(val: &OsString) -> anyhow::Result<Rev> {
let s = val.to_str().ok_or(anyhow!("invalid git rev {val:?}"))?;
Ok(Rev::from(s.to_owned()))
}
pub fn oid(val: &OsString) -> anyhow::Result<Rev> {
let s = string(val);
let _ = radicle::git::Oid::from_str(&s).map_err(|_| anyhow!("invalid git oid '{s}'"))?;
Ok(Rev::from(s))
}
pub fn alias(val: &OsString) -> anyhow::Result<Alias> {
let val = val.as_os_str();
let val = val
.to_str()
.ok_or_else(|| anyhow!("alias must be valid UTF-8"))?;
Alias::from_str(val).map_err(|e| e.into())
}
#[allow(dead_code)]
pub fn issue(val: &OsString) -> anyhow::Result<issue::IssueId> {
let val = val.to_string_lossy();
issue::IssueId::from_str(&val).map_err(|_| anyhow!("invalid Issue ID '{}'", val))
}
#[allow(dead_code)]
pub fn patch(val: &OsString) -> anyhow::Result<patch::PatchId> {
let val = val.to_string_lossy();
patch::PatchId::from_str(&val).map_err(|_| anyhow!("invalid Patch ID '{}'", val))
}
pub fn cob(val: &OsString) -> anyhow::Result<cob::ObjectId> {
let val = val.to_string_lossy();
cob::ObjectId::from_str(&val).map_err(|_| anyhow!("invalid Object ID '{}'", val))
}
+2
View File
@@ -14,6 +14,7 @@ pub struct PassphraseValidator {
keystore: Keystore,
}
#[allow(dead_code)]
impl PassphraseValidator {
/// Create a new validator.
pub fn new(keystore: Keystore) -> Self {
@@ -39,6 +40,7 @@ impl inquire::validator::StringValidator for PassphraseValidator {
/// Get the signer. First we try getting it from ssh-agent, otherwise we prompt the user,
/// if we're connected to a TTY.
#[allow(dead_code)]
pub fn signer(profile: &Profile) -> anyhow::Result<Box<dyn Signer>> {
if let Ok(signer) = profile.signer() {
return Ok(signer);
+1
View File
@@ -10,6 +10,7 @@ use tuirealm::{Application, EventListenerCfg, NoUserEvent};
pub mod cob;
pub mod context;
pub mod log;
pub mod ui;
use context::Context;
+15
View File
@@ -0,0 +1,15 @@
use log::LevelFilter;
use radicle::profile::Profile;
pub fn enable(cmd: &str, op: &str, profile: &Profile) -> Result<(), anyhow::Error> {
let logfile = format!(
"{}/rad-tui-{}-{}.log",
profile.home().path().to_string_lossy(),
cmd,
op,
);
simple_logging::log_to_file(logfile, LevelFilter::Info)?;
Ok(())
}