Added clap for CLI

This commit is contained in:
Daniel Flanagan 2024-04-16 22:49:24 -05:00
parent 3fe83e83f1
commit 1b70e7c9bd
10 changed files with 314 additions and 98 deletions

118
Cargo.lock generated
View file

@ -59,6 +59,54 @@ dependencies = [
"libc",
]
[[package]]
name = "anstream"
version = "0.6.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc"
[[package]]
name = "anstyle-parse"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648"
dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7"
dependencies = [
"anstyle",
"windows-sys 0.52.0",
]
[[package]]
name = "anyhow"
version = "1.0.81"
@ -184,6 +232,46 @@ dependencies = [
"chrono",
]
[[package]]
name = "clap"
version = "4.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
[[package]]
name = "color-eyre"
version = "0.6.3"
@ -211,6 +299,12 @@ dependencies = [
"tracing-error",
]
[[package]]
name = "colorchoice"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
[[package]]
name = "compact_str"
version = "0.7.1"
@ -546,6 +640,12 @@ version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
version = "0.3.4"
@ -1495,6 +1595,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "strum"
version = "0.26.2"
@ -1510,7 +1616,7 @@ version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6cf59daf282c0a494ba14fd21610a0325f9f90ec9d1231dea26bcb1d696c946"
dependencies = [
"heck",
"heck 0.4.1",
"proc-macro2",
"quote",
"rustversion",
@ -1565,12 +1671,14 @@ dependencies = [
]
[[package]]
name = "tasks"
name = "taskr"
version = "0.1.0"
dependencies = [
"anyhow",
"chrono",
"chrono-humanize",
"clap",
"clap_derive",
"color-eyre",
"crossterm",
"ratatui",
@ -1892,6 +2000,12 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "utf8parse"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "valuable"
version = "0.1.0"

View file

@ -1,14 +1,14 @@
[package]
name = "tasks"
name = "taskr"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = "1.0.81"
chrono = { version = "0.4.35", features = ["serde"] }
chrono-humanize = "0.2.3"
clap = { version = "4.5.4", features = ["derive"] }
clap_derive = "4.5.4"
color-eyre = "0.6.3"
crossterm = "0.27.0"
ratatui = "0.26.2"

65
src/cli.rs Normal file
View file

@ -0,0 +1,65 @@
use crate::prelude::*;
use clap::{Args, Parser, Subcommand};
// TODO: clap for CLI
#[derive(Parser)]
#[command(version, about, long_about = None)]
#[command(propagate_version = true)]
pub struct Cli {
#[command(subcommand)]
pub command: Commands,
/// Tell taskr which directory to write log files to
#[arg(long, default_value = None)]
pub logs_directory: Option<String>,
}
#[derive(Subcommand)]
pub enum Commands {
/// Run the interactive terminal user interface (TUI)
Ui(UiArgs),
/// Lists tasks with options for syncing
List(ListArgs),
}
#[derive(Args)]
pub struct UiArgs {}
#[derive(Args, Debug)]
pub struct ListArgs {
#[arg(long, default_value_t = false)]
pub sync: bool,
}
impl Cli {
pub fn new() -> Self {
Self::parse()
}
}
pub async fn list_tasks(args: &ListArgs) -> Result<()> {
let tasks = crate::tasks::Tasks::try_new()?;
if args.sync {
eprintln!("Syncing...");
tasks.sync().await?;
}
// TODO: if we _don't_ sync, check last sync time and let user know
// that things may have changed
let tasks = tasks.all()?;
eprintln!("{} Tasks", tasks.len());
// TODO: make this generical? take a vec of vecs or something, scan each
// entry and their lengths and we can spit out a nice table with each
// "column" having equal length
for (_, t) in tasks.iter() {
println!("{t}");
}
Ok(())
}
#[test]
fn verify_cli() {
use clap::CommandFactory;
Cli::command().debug_assert()
}

View file

@ -139,9 +139,16 @@ impl Jira {
Ok(issues)
}
pub async fn issue(&self, key: &str) -> Result<Issue> {
self.client
.build(Method::GET, &format!("/rest/api/3/issue/{key}"))?
.res::<Issue>()
.await
}
// TODO: move this somewhere nicer?
pub fn by_key(issues: Vec<Issue>) -> Result<HashMap<String, Issue>> {
Ok(issues.into_iter().map(|i| (i.key.to_owned(), i)).collect())
pub fn by_key(issues: Vec<Issue>) -> HashMap<String, Issue> {
issues.into_iter().map(|i| (i.key.to_owned(), i)).collect()
}
pub async fn assigned_open_issues(&self) -> Result<Vec<Issue>> {

View file

@ -1,104 +1,27 @@
#![warn(clippy::all)]
mod cli;
mod client;
mod config;
mod gitlab;
mod jira;
mod observe;
mod prelude;
mod result;
mod task;
mod tasks;
mod tui;
use crate::result::Result;
use tasks::Tasks;
use tracing::{error, info};
use tracing_subscriber::{filter::LevelFilter, EnvFilter};
use crossterm::{
event::{self, KeyCode, KeyEventKind},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
};
use ratatui::{
prelude::{CrosstermBackend, Stylize, Terminal},
widgets::Paragraph,
};
use std::io::stdout;
#[allow(dead_code)]
const ANSI_CLEAR: &'static str = "\x1b[2J\x1b[1;1H";
use crate::prelude::*;
use cli::{Cli, Commands};
#[tokio::main]
async fn main() -> Result<()> {
let logs_dir = xdg::BaseDirectories::new()?.create_cache_directory("taskr/logs")?;
let file_appender = tracing_appender::rolling::hourly(logs_dir, "log");
let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);
color_eyre::install().expect("Failed to install color_eyre");
let filter = EnvFilter::builder()
.with_default_directive(LevelFilter::TRACE.into())
.parse_lossy("info,tasks=trace");
tracing_subscriber::fmt()
.with_writer(non_blocking)
.with_env_filter(filter)
.init();
match run().await {
Ok(()) => Ok(()),
Err(err) => {
error!("{err}");
Err(err)
let cli = Cli::new();
observe::setup_logging(cli.logs_directory)?;
trace!("Starting...");
match cli.command {
Commands::Ui(_args) => tui::run().await,
Commands::List(args) => cli::list_tasks(&args).await,
}
}
}
async fn run() -> Result<()> {
let t = Tasks::try_new()?;
// print!("{ANSI_CLEAR}");
// let gitlab_user = tasks.gitlab.me().await?;
// info!("{gitlab_user:#?}");
// let jira_user = tasks.jira.me().await?;
// tasks.purge_all()?;
let tasks = t.all()?;
if tasks.len() < 1 {
info!("{:?}", t.sync().await?);
}
let mut vtasks: Vec<&task::Task> = tasks.values().collect();
vtasks.sort_unstable();
for t in &vtasks {
info!("{}", t);
}
info!("Number of tasks: {}", vtasks.len());
tui().await
}
async fn tui() -> Result<()> {
stdout().execute(EnterAlternateScreen)?;
enable_raw_mode()?;
let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
terminal.clear()?;
loop {
terminal.draw(|frame| {
let area = frame.size();
frame.render_widget(
Paragraph::new("Hello Ratatui! (press 'q' to quit)").white(),
area,
);
})?;
if event::poll(std::time::Duration::from_millis(10))? {
if let event::Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
break;
}
}
}
}
// TODO main loop
stdout().execute(LeaveAlternateScreen)?;
disable_raw_mode()?;
Ok(())
}

32
src/observe.rs Normal file
View file

@ -0,0 +1,32 @@
use crate::prelude::*;
use std::path::PathBuf;
use tracing_subscriber::{filter::LevelFilter, EnvFilter};
pub fn setup_logging<T: Into<PathBuf>>(logs_directory: Option<T>) -> Result<()> {
let default_logs_directory = || -> Result<PathBuf> {
Ok(xdg::BaseDirectories::new()?.create_cache_directory("taskr/logs")?)
};
let logs_directory: PathBuf = match logs_directory {
Some(p) => p.into(),
None => default_logs_directory()?,
};
if !logs_directory.exists() {
std::fs::create_dir_all(&logs_directory)?;
}
let file_appender = tracing_appender::rolling::hourly(logs_directory, "log");
let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender);
color_eyre::install().expect("Failed to install color_eyre");
let filter = EnvFilter::builder()
.with_default_directive(LevelFilter::TRACE.into())
.parse_lossy("info,taskr=trace");
tracing_subscriber::fmt()
.with_writer(non_blocking)
.with_env_filter(filter)
.init();
Ok(())
}

4
src/prelude.rs Normal file
View file

@ -0,0 +1,4 @@
#![allow(unused_imports)]
pub use crate::result::Result;
pub use tracing::{debug, error, info, trace, warn};

View file

@ -69,7 +69,7 @@ impl Display for Task {
tags,
} = self;
f.write_fmt(format_args!(
"{jira_key}: <{status}> {description} [p{jira_priority}]",
"{jira_key} {status:>10} {jira_priority} {description}",
))
}
}

View file

@ -29,6 +29,8 @@ pub struct Desyncs {
impl Tasks {
pub fn try_new() -> Result<Self> {
// TODO: cache, use keyring, talk to a daemon, or otherwise cache this safely
// TODO: or find a way to more-lazily load the token?
let gl_token = env::var("GITLAB_TOKEN").or_else(|_| -> Result<String> {
let output = Command::new("pass")
.arg("client/divvy/gitlab-glpat")
@ -38,6 +40,8 @@ impl Tasks {
})?;
let gitlab = GitLab::try_new("https://git.hq.bill.com/api/v4", &gl_token)?;
// TODO: cache, use keyring, talk to a daemon, or otherwise cache this safely
// TODO: or find a way to more-lazily load the token?
let jira_token = env::var("JIRA_TOKEN").or_else(|_| -> Result<String> {
let output = Command::new("pass")
.arg("client/divvy/jira-api-token-with-email")
@ -129,6 +133,8 @@ impl Tasks {
/// for a task that has no associated open and appropriately assigned jira issue
async fn fix_dangling_task(&self, task: &Task) -> Result<()> {
// check if closed, if it is, delete the task
let issue = self.jira.issue(&task.jira_key).await?;
Ok(())
}
@ -136,7 +142,7 @@ impl Tasks {
/// for use when sync'ing local tasks to remote state (jira, gitlab)
pub async fn sync(&self) -> Result<()> {
let mut tasks = self.all()?;
let issues = crate::jira::Jira::by_key(self.jira.assigned_open_issues().await?)?;
let issues = crate::jira::Jira::by_key(self.jira.assigned_open_issues().await?);
let task_keys: HashSet<String, RandomState> =
HashSet::from_iter(tasks.keys().map(|s| s.to_owned()));
let issue_keys: HashSet<String, RandomState> =

65
src/tui.rs Normal file
View file

@ -0,0 +1,65 @@
use crate::prelude::*;
use crate::task;
use crate::tasks::Tasks;
use crossterm::{
event::{self, KeyCode, KeyEventKind},
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
ExecutableCommand,
};
use ratatui::{
prelude::{CrosstermBackend, Stylize, Terminal},
widgets::Paragraph,
};
use std::io::stdout;
pub async fn run() -> Result<()> {
let t = Tasks::try_new()?;
// print!("{ANSI_CLEAR}");
// let gitlab_user = tasks.gitlab.me().await?;
// info!("{gitlab_user:#?}");
// let jira_user = tasks.jira.me().await?;
// tasks.purge_all()?;
let tasks = t.all()?;
if tasks.len() < 1 {
info!("{:?}", t.sync().await?);
}
let mut vtasks: Vec<&task::Task> = tasks.values().collect();
vtasks.sort_unstable();
for t in &vtasks {
info!("{}", t);
}
info!("Number of tasks: {}", vtasks.len());
tui().await
}
async fn tui() -> Result<()> {
stdout().execute(EnterAlternateScreen)?;
enable_raw_mode()?;
let mut terminal = Terminal::new(CrosstermBackend::new(stdout()))?;
terminal.clear()?;
loop {
terminal.draw(|frame| {
let area = frame.size();
frame.render_widget(
Paragraph::new("Hello Ratatui! (press 'q' to quit)").white(),
area,
);
})?;
if event::poll(std::time::Duration::from_millis(10))? {
if let event::Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press && key.code == KeyCode::Char('q') {
break;
}
}
}
}
// TODO main loop
stdout().execute(LeaveAlternateScreen)?;
disable_raw_mode()?;
Ok(())
}