Added clap for CLI
This commit is contained in:
parent
3fe83e83f1
commit
1b70e7c9bd
10 changed files with 314 additions and 98 deletions
118
Cargo.lock
generated
118
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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
65
src/cli.rs
Normal 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()
|
||||
}
|
11
src/jira.rs
11
src/jira.rs
|
@ -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>> {
|
||||
|
|
101
src/main.rs
101
src/main.rs
|
@ -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
32
src/observe.rs
Normal 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
4
src/prelude.rs
Normal file
|
@ -0,0 +1,4 @@
|
|||
#![allow(unused_imports)]
|
||||
|
||||
pub use crate::result::Result;
|
||||
pub use tracing::{debug, error, info, trace, warn};
|
|
@ -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}",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
65
src/tui.rs
Normal 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(())
|
||||
}
|
Loading…
Reference in a new issue