diff --git a/Cargo.lock b/Cargo.lock index ff19097..bc971e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index de4ae85..3da2c16 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..c70b1d3 --- /dev/null +++ b/src/cli.rs @@ -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, +} + +#[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() +} diff --git a/src/jira.rs b/src/jira.rs index 3b9f8e2..cf56e54 100644 --- a/src/jira.rs +++ b/src/jira.rs @@ -139,9 +139,16 @@ impl Jira { Ok(issues) } + pub async fn issue(&self, key: &str) -> Result { + self.client + .build(Method::GET, &format!("/rest/api/3/issue/{key}"))? + .res::() + .await + } + // TODO: move this somewhere nicer? - pub fn by_key(issues: Vec) -> Result> { - Ok(issues.into_iter().map(|i| (i.key.to_owned(), i)).collect()) + pub fn by_key(issues: Vec) -> HashMap { + issues.into_iter().map(|i| (i.key.to_owned(), i)).collect() } pub async fn assigned_open_issues(&self) -> Result> { diff --git a/src/main.rs b/src/main.rs index 5b37c2b..0a7af34 100644 --- a/src/main.rs +++ b/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(()) -} diff --git a/src/observe.rs b/src/observe.rs new file mode 100644 index 0000000..bac7c10 --- /dev/null +++ b/src/observe.rs @@ -0,0 +1,32 @@ +use crate::prelude::*; + +use std::path::PathBuf; +use tracing_subscriber::{filter::LevelFilter, EnvFilter}; + +pub fn setup_logging>(logs_directory: Option) -> Result<()> { + let default_logs_directory = || -> Result { + 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(()) +} diff --git a/src/prelude.rs b/src/prelude.rs new file mode 100644 index 0000000..f51d816 --- /dev/null +++ b/src/prelude.rs @@ -0,0 +1,4 @@ +#![allow(unused_imports)] + +pub use crate::result::Result; +pub use tracing::{debug, error, info, trace, warn}; diff --git a/src/task.rs b/src/task.rs index e6df9f2..2b0547d 100644 --- a/src/task.rs +++ b/src/task.rs @@ -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}", )) } } diff --git a/src/tasks.rs b/src/tasks.rs index 1dd3c1e..24c69d0 100644 --- a/src/tasks.rs +++ b/src/tasks.rs @@ -29,6 +29,8 @@ pub struct Desyncs { impl Tasks { pub fn try_new() -> Result { + // 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 { 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 { 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 = HashSet::from_iter(tasks.keys().map(|s| s.to_owned())); let issue_keys: HashSet = diff --git a/src/tui.rs b/src/tui.rs new file mode 100644 index 0000000..7f83757 --- /dev/null +++ b/src/tui.rs @@ -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(()) +}