Layout missing commands

This commit is contained in:
Bodo Junglas
2020-02-02 20:45:17 +01:00
parent 0ba6be6765
commit c19ec08fad
13 changed files with 200 additions and 21 deletions
+31
View File
@@ -2,6 +2,11 @@ function __fish_is_arg_n --argument-names n
test $n -eq (count (string match -v -- '-*' (commandline -poc)))
end
function __fish_arg_n --argument-names n
set -l args (string match -v -- '-*' (commandline -poc))
echo -n $args[(math "$n + 1")]
end
# options
complete -c acari -s o -l output -a "flat json pretty" -d "set output format"
complete -c acari -s h -l help -d "show help"
@@ -12,8 +17,13 @@ complete -f -c acari -n "__fish_use_subcommand" -a init -d "initialize connectio
complete -f -c acari -n "__fish_use_subcommand" -a check -d "check connection"
complete -f -c acari -n "__fish_use_subcommand" -a clear-cache -d "Clear local cache"
complete -f -c acari -n "__fish_use_subcommand" -a customers -d "list customers"
complete -f -c acari -n "__fish_use_subcommand" -a entries -d "list time entries"
complete -f -c acari -n "__fish_use_subcommand" -a projects -d "list projects"
complete -f -c acari -n "__fish_use_subcommand" -a services -d "list services"
complete -f -c acari -n "__fish_use_subcommand" -a set -d "set time entry"
complete -f -c acari -n "__fish_use_subcommand" -a start -d "start time tracking"
complete -f -c acari -n "__fish_use_subcommand" -a stop -d "stop time tracking"
complete -f -c acari -n "__fish_use_subcommand" -a tracking -d "show current time tracking"
# check
complete -f -c acari -n "__fish_seen_subcommand_from check"
@@ -24,9 +34,30 @@ complete -f -c acari -n "__fish_seen_subcommand_from clear-cache"
# customers
complete -f -c acari -n "__fish_seen_subcommand_from customers"
# entries
complete -f -c acari -n "__fish_seen_subcommand_from entries"
# projects
complete -f -c acari -n "__fish_seen_subcommand_from projects"
complete -f -c acari -n "__fish_seen_subcommand_from projects; and __fish_is_arg_n 2" -a "(acari -oflat customers)"
# services
complete -f -c acari -n "__fish_seen_subcommand_from services"
# set
complete -f -c acari -n "__fish_seen_subcommand_from set"
complete -f -c acari -n "__fish_seen_subcommand_from set; and __fish_is_arg_n 2" -a "(acari -oflat customers)"
complete -f -c acari -n "__fish_seen_subcommand_from set; and __fish_is_arg_n 3" -a "(acari -oflat projects (__fish_arg_n 2))"
complete -f -c acari -n "__fish_seen_subcommand_from set; and __fish_is_arg_n 4" -a "(acari -oflat services)"
# start
complete -f -c acari -n "__fish_seen_subcommand_from start"
complete -f -c acari -n "__fish_seen_subcommand_from start; and __fish_is_arg_n 2" -a "(acari -oflat customers)"
complete -f -c acari -n "__fish_seen_subcommand_from start; and __fish_is_arg_n 3" -a "(acari -oflat projects (__fish_arg_n 2))"
complete -f -c acari -n "__fish_seen_subcommand_from start; and __fish_is_arg_n 4" -a "(acari -oflat services)"
# stop
complete -f -c acari -n "__fish_seen_subcommand_from stop"
# tracking
complete -f -c acari -n "__fish_seen_subcommand_from tracking"
+8
View File
@@ -6,6 +6,10 @@ mod entries;
mod init;
mod projects_of_customer;
mod services;
mod set;
mod start;
mod stop;
mod tracking;
pub use all_projects::*;
pub use check::*;
@@ -15,6 +19,10 @@ pub use entries::*;
pub use init::*;
pub use projects_of_customer::*;
pub use services::*;
pub use set::*;
pub use start::*;
pub use stop::*;
pub use tracking::*;
use acari_lib::AcariError;
+1 -1
View File
@@ -2,7 +2,7 @@ use super::OutputFormat;
use acari_lib::{AcariError, Client, Project};
use prettytable::{cell, format, row, Table};
pub fn projects_of_customer(client: &dyn Client, customer_name: &str, output_format: OutputFormat) -> Result<(), AcariError> {
pub fn projects_of_customer(client: &dyn Client, output_format: OutputFormat, customer_name: &str) -> Result<(), AcariError> {
let mut projects = client.get_projects()?;
projects.retain(|p| p.customer_name == customer_name);
+14
View File
@@ -0,0 +1,14 @@
use super::OutputFormat;
use acari_lib::{AcariError, Client, Tracker};
use prettytable::{cell, format, row, Table};
pub fn set(
client: &dyn Client,
output_format: OutputFormat,
customer_name: &str,
project_name: &str,
service_name: &str,
minutes: u32,
) -> Result<(), AcariError> {
Ok(())
}
+14
View File
@@ -0,0 +1,14 @@
use super::OutputFormat;
use acari_lib::{AcariError, Client, Tracker};
use prettytable::{cell, format, row, Table};
pub fn start(
client: &dyn Client,
output_format: OutputFormat,
customer_name: &str,
project_name: &str,
service_name: &str,
minutes_offset: u32,
) -> Result<(), AcariError> {
Ok(())
}
+7
View File
@@ -0,0 +1,7 @@
use super::OutputFormat;
use acari_lib::{AcariError, Client, Tracker};
use prettytable::{cell, format, row, Table};
pub fn stop(client: &dyn Client, output_format: OutputFormat) -> Result<(), AcariError> {
Ok(())
}
+7
View File
@@ -0,0 +1,7 @@
use super::OutputFormat;
use acari_lib::{AcariError, Client, Tracker};
use prettytable::{cell, format, row, Table};
pub fn tracking(client: &dyn Client, output_format: OutputFormat) -> Result<(), AcariError> {
Ok(())
}
+2 -2
View File
@@ -1,4 +1,4 @@
use acari_lib::{AcariError, CachedClient, Client, StdClient};
use acari_lib::{internal_error, AcariError, CachedClient, Client, StdClient};
use serde::{Deserialize, Serialize};
use std::fs::{self, File};
use std::io::{self, Read, Write};
@@ -46,7 +46,7 @@ impl Config {
fs::create_dir_all(
&config_file
.parent()
.ok_or_else(|| AcariError::InternalError("Invalid config path".to_string()))?,
.ok_or_else(|| internal_error!("Invalid config path: {}", config_file.to_string_lossy()))?,
)?;
let mut file = File::create(&config_file)?;
+73 -15
View File
@@ -1,5 +1,5 @@
use acari_lib::{AcariError, DateSpan};
use clap::{App, Arg, SubCommand};
use acari_lib::{user_error, AcariError, DateSpan};
use clap::{App, Arg, ArgMatches, SubCommand};
mod commands;
mod config;
@@ -23,12 +23,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.subcommand(SubCommand::with_name("check").about("Check connection to mite"))
.subcommand(SubCommand::with_name("clear-cache").about("Clear the local cache"))
.subcommand(SubCommand::with_name("customers").about("List all customers"))
.subcommand(
SubCommand::with_name("projects")
.arg(Arg::with_name("customer").help("Optional: List only projects of a specific customer"))
.about("List all projects"),
)
.subcommand(SubCommand::with_name("services").about("List all services"))
.subcommand(
SubCommand::with_name("entries")
.arg(
@@ -37,7 +31,32 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
.help("Date span to query\n(today, yesterday, this-week, last-week,\n this-month, last-month, yyyy-mm-dd, yyyy-mm-dd|yyyy-mm-dd)"),
)
.about("Query time entries"),
);
)
.subcommand(
SubCommand::with_name("projects")
.arg(Arg::with_name("customer").help("Optional: List only projects of a specific customer"))
.about("List all projects"),
)
.subcommand(SubCommand::with_name("services").about("List all services"))
.subcommand(
SubCommand::with_name("set")
.arg(Arg::with_name("customer").required(true).help("Customer name"))
.arg(Arg::with_name("project").required(true).help("Project name"))
.arg(Arg::with_name("service").required(true).help("Service name"))
.arg(Arg::with_name("time").required(true).help("Time (minutes or hh:mm)"))
.about("Start tracking time"),
)
.subcommand(
SubCommand::with_name("start")
.arg(Arg::with_name("customer").required(true).help("Customer name"))
.arg(Arg::with_name("project").required(true).help("Project name"))
.arg(Arg::with_name("service").required(true).help("Service name"))
.arg(Arg::with_name("offset").help("Optional: Starting offset (minutes or hh:mm)"))
.about("Start tracking time"),
)
.subcommand(SubCommand::with_name("stop").about("Stop current time tracking"))
.subcommand(SubCommand::with_name("tracking").about("Show currently tracked time entry"));
let matches = app.get_matches();
let output_format = matches.value_of("output").map(OutputFormat::from_string).unwrap_or(Ok(OutputFormat::Pretty))?;
@@ -50,17 +69,33 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
("check", _) => commands::check(client.as_ref(), output_format)?,
("clear-cache", _) => commands::clear_cache()?,
("customers", _) => commands::customers(client.as_ref(), output_format)?,
("entries", Some(sub_matches)) => {
let span = required_arg(sub_matches, "span")?;
commands::entries(client.as_ref(), output_format, DateSpan::from_string(span)?)?;
}
("projects", Some(sub_matches)) => match sub_matches.value_of("customer") {
Some(customer) => commands::projects_of_customer(client.as_ref(), customer, output_format)?,
Some(customer) => commands::projects_of_customer(client.as_ref(), output_format, customer)?,
None => commands::all_projects(client.as_ref(), output_format)?,
},
("services", _) => commands::services(client.as_ref(), output_format)?,
("entries", Some(sub_matches)) => {
let span_arg = sub_matches
.value_of("span")
.ok_or(AcariError::UserError("Missing <span> argument".to_string()))?;
commands::entries(client.as_ref(), output_format, DateSpan::from_string(span_arg)?)?;
("set", Some(sub_matches)) => {
let customer = required_arg(sub_matches, "customer")?;
let project = required_arg(sub_matches, "project")?;
let service = required_arg(sub_matches, "service")?;
let time = parse_minutes(required_arg(sub_matches, "time")?)?;
commands::set(client.as_ref(), output_format, customer, project, service, time)?;
}
("start", Some(sub_matches)) => {
let customer = required_arg(sub_matches, "customer")?;
let project = required_arg(sub_matches, "project")?;
let service = required_arg(sub_matches, "service")?;
let offset = minutes_arg(sub_matches, "offset")?;
commands::start(client.as_ref(), output_format, customer, project, service, offset.unwrap_or(0))?;
}
("stop", _) => commands::stop(client.as_ref(), output_format)?,
("tracking", _) => commands::tracking(client.as_ref(), output_format)?,
(invalid, _) => Err(AcariError::UserError(format!("Unknown command: {}", invalid)))?,
}
}
@@ -72,3 +107,26 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}
fn required_arg<'a>(matches: &'a ArgMatches, name: &str) -> Result<&'a str, AcariError> {
matches.value_of(name).ok_or_else(|| user_error!("Missing <{}> argument", name))
}
fn minutes_arg(matches: &ArgMatches, name: &str) -> Result<Option<u32>, AcariError> {
match matches.value_of(name) {
Some(value) => parse_minutes(value).map(Some),
None => Ok(None),
}
}
fn parse_minutes(expr: &str) -> Result<u32, AcariError> {
match expr.find(":") {
Some(idx) => {
let hours = expr[..idx].parse::<u32>().map_err(|e| user_error!("Invalid time format: {}", e))?;
let minutes = expr[idx + 1..].parse::<u32>().map_err(|e| user_error!("Invalid time format: {}", e))?;
Ok(hours * 60 + minutes)
}
None => expr.parse::<u32>().map_err(|e| user_error!("Invalid time format: {}", e)),
}
}
+9 -1
View File
@@ -1,5 +1,5 @@
use crate::error::AcariError;
use crate::model::{Account, Customer, Project, Service, TimeEntry, User};
use crate::model::{Account, Customer, Project, Service, TimeEntry, Tracker, User};
use crate::query::DateSpan;
use crate::std_client::StdClient;
use crate::Client;
@@ -81,9 +81,17 @@ impl Client for CachedClient {
self.cache_data("services.json", || self.client.get_services())
}
fn get_time_entry(&self, entry_id: u32) -> Result<TimeEntry, AcariError> {
self.client.get_time_entry(entry_id) // This should not be cached
}
fn get_time_entries(&self, date_span: DateSpan) -> Result<Vec<TimeEntry>, AcariError> {
self.client.get_time_entries(date_span) // This should not be cached
}
fn get_tracker(&self) -> Result<Tracker, AcariError> {
self.client.get_tracker() // This should not be cached
}
}
fn file_age(path: &PathBuf) -> Result<Option<Duration>, AcariError> {
+18
View File
@@ -21,5 +21,23 @@ pub trait Client {
fn get_services(&self) -> Result<Vec<Service>, AcariError>;
fn get_time_entry(&self, entry_id: u32) -> Result<TimeEntry, AcariError>;
fn get_time_entries(&self, date_span: DateSpan) -> Result<Vec<TimeEntry>, AcariError>;
fn get_tracker(&self) -> Result<Tracker, AcariError>;
}
#[macro_export]
macro_rules! user_error {
( $( $arg:expr ),* ) => {
AcariError::UserError(format!($($arg),*))
}
}
#[macro_export]
macro_rules! internal_error {
( $( $arg:expr ),* ) => {
AcariError::InternalError(format!($($arg),*))
}
}
+1 -1
View File
@@ -13,7 +13,7 @@ impl Day {
match day.to_lowercase().as_str() {
"today" | "now" => Ok(Day::Today),
"yesterday" => Ok(Day::Yesterday),
date => Ok(Day::Date(NaiveDate::parse_from_str(date, "%Y-%m-%d")?))
date => Ok(Day::Date(NaiveDate::parse_from_str(date, "%Y-%m-%d")?)),
}
}
+15 -1
View File
@@ -1,5 +1,5 @@
use crate::error::AcariError;
use crate::model::{Account, Customer, MiteEntity, Project, Service, TimeEntry, User};
use crate::model::{Account, Customer, MiteEntity, Project, Service, TimeEntry, Tracker, User};
use crate::query::DateSpan;
use crate::Client;
use serde::de::DeserializeOwned;
@@ -91,6 +91,13 @@ impl Client for StdClient {
)
}
fn get_time_entry(&self, entry_id: u32) -> Result<TimeEntry, AcariError> {
match self.get(&format!("/time_entries/{}.json", entry_id))? {
MiteEntity::TimeEntry(time_entry) => Ok(time_entry),
response => Err(AcariError::Mite(400, format!("Unexpected response: {:?}", response))),
}
}
fn get_time_entries(&self, date_span: DateSpan) -> Result<Vec<TimeEntry>, AcariError> {
Ok(
self
@@ -103,6 +110,13 @@ impl Client for StdClient {
.collect(),
)
}
fn get_tracker(&self) -> Result<Tracker, AcariError> {
match self.get("/tracker.json")? {
MiteEntity::Tracker(tracker) => Ok(tracker),
response => Err(AcariError::Mite(400, format!("Unexpected response: {:?}", response))),
}
}
}
fn handle_response<T: DeserializeOwned>(response: blocking::Response) -> Result<T, AcariError> {