implement t configure
This commit is contained in:
parent
4ebf0681c2
commit
23a3603628
|
@ -23,6 +23,7 @@ pub mod kill;
|
|||
pub mod now;
|
||||
pub mod edit;
|
||||
pub mod archive;
|
||||
pub mod configure;
|
||||
|
||||
pub trait Command<'a> {
|
||||
type Args: TryFrom<&'a ArgMatches<'a>>;
|
||||
|
|
|
@ -0,0 +1,166 @@
|
|||
use std::convert::TryFrom;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::ArgMatches;
|
||||
use chrono::{DateTime, Utc};
|
||||
|
||||
use crate::database::Database;
|
||||
use crate::error::{Error, Result};
|
||||
use crate::commands::Command;
|
||||
use crate::config::{Config, WeekDay};
|
||||
use crate::formatters::Formatter;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Args {
|
||||
database_file: Option<PathBuf>,
|
||||
round_in_seconds: Option<u32>,
|
||||
append_notes_delimiter: Option<String>,
|
||||
formatter_search_paths: Option<Vec<PathBuf>>,
|
||||
default_formatter: Option<Formatter>,
|
||||
auto_sheet: Option<String>,
|
||||
auto_sheet_search_paths: Option<Vec<PathBuf>>,
|
||||
default_command: Option<String>,
|
||||
auto_checkout: Option<bool>,
|
||||
require_note: Option<bool>,
|
||||
note_editor: Option<String>,
|
||||
week_start: Option<WeekDay>,
|
||||
}
|
||||
|
||||
impl Args {
|
||||
/// returns true only if no argument was passed other that possibly --id.
|
||||
/// This means that an edit was requested without specifying what to edit,
|
||||
/// therefore let's edit the note because why not
|
||||
fn none_given(&self) -> bool {
|
||||
!(
|
||||
self.database_file.is_some() ||
|
||||
self.round_in_seconds.is_some() ||
|
||||
self.append_notes_delimiter.is_some() ||
|
||||
self.formatter_search_paths.is_some() ||
|
||||
self.default_formatter.is_some() ||
|
||||
self.auto_sheet.is_some() ||
|
||||
self.auto_sheet_search_paths.is_some() ||
|
||||
self.default_command.is_some() ||
|
||||
self.auto_checkout.is_some() ||
|
||||
self.require_note.is_some() ||
|
||||
self.note_editor.is_some() ||
|
||||
self.week_start.is_some()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn yes_no_none(matches: &ArgMatches, opt: &str) -> Option<bool> {
|
||||
if matches.is_present(opt) {
|
||||
Some(true)
|
||||
} else if matches.is_present(&format!("no_{}", opt)) {
|
||||
Some(false)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a ArgMatches<'a>> for Args {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(matches: &'a ArgMatches) -> Result<Self> {
|
||||
Ok(Args {
|
||||
database_file: matches.value_of("database_file").map(|e| e.into()),
|
||||
round_in_seconds: matches.value_of("round_in_seconds").map(|v| v.parse().unwrap()),
|
||||
append_notes_delimiter: matches.value_of("append_notes_delimiter").map(|v| v.to_owned()),
|
||||
formatter_search_paths: matches.values_of("formatter_search_paths").map(|v| v.map(|a| a.into()).collect()),
|
||||
default_formatter: matches.value_of("default_formatter").map(|v| v.parse().unwrap()),
|
||||
auto_sheet: matches.value_of("auto_sheet").map(|v| v.to_owned()),
|
||||
auto_sheet_search_paths: matches.values_of("auto_sheet_search_paths").map(|v| v.map(|a| a.into()).collect()),
|
||||
default_command: matches.value_of("default_command").map(|v| v.to_owned()),
|
||||
auto_checkout: yes_no_none(matches, "auto_checkout"),
|
||||
require_note: yes_no_none(matches, "require_note"),
|
||||
note_editor: matches.value_of("note_editor").map(|v| v.to_owned()),
|
||||
week_start: matches.value_of("week_start").map(|w| w.parse()).transpose()?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ConfigureCommand {}
|
||||
|
||||
impl<'a> Command<'a> for ConfigureCommand {
|
||||
type Args = Args;
|
||||
|
||||
fn handle<D, O, E>(args: Args, _db: &mut D, out: &mut O, _err: &mut E, config: &Config, _now: DateTime<Utc>) -> Result<()>
|
||||
where
|
||||
D: Database,
|
||||
O: Write,
|
||||
E: Write,
|
||||
{
|
||||
if args.none_given() {
|
||||
if let Some(path) = config.path.as_deref() {
|
||||
writeln!(out, "{}", path.display())?;
|
||||
} else {
|
||||
writeln!(out, "Config file is in memory")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
let mut new_config = config.clone();
|
||||
|
||||
if let Some(path) = args.database_file {
|
||||
new_config.database_file = path;
|
||||
}
|
||||
|
||||
if let Some(val) = args.round_in_seconds {
|
||||
new_config.round_in_seconds = val;
|
||||
}
|
||||
|
||||
if let Some(val) = args.append_notes_delimiter {
|
||||
new_config.append_notes_delimiter = val;
|
||||
}
|
||||
|
||||
if let Some(val) = args.formatter_search_paths {
|
||||
new_config.formatter_search_paths = val;
|
||||
}
|
||||
|
||||
if let Some(val) = args.default_formatter {
|
||||
new_config.default_formatter = val;
|
||||
}
|
||||
|
||||
if let Some(val) = args.auto_sheet {
|
||||
new_config.auto_sheet = val;
|
||||
}
|
||||
|
||||
if let Some(val) = args.auto_sheet_search_paths {
|
||||
new_config.auto_sheet_search_paths = val;
|
||||
}
|
||||
|
||||
if let Some(val) = args.default_command {
|
||||
new_config.default_command = Some(val);
|
||||
}
|
||||
|
||||
if let Some(val) = args.auto_checkout {
|
||||
new_config.auto_checkout = val;
|
||||
}
|
||||
|
||||
if let Some(val) = args.require_note {
|
||||
new_config.require_note = val;
|
||||
}
|
||||
|
||||
if let Some(val) = args.note_editor {
|
||||
new_config.note_editor = Some(val);
|
||||
}
|
||||
|
||||
if let Some(val) = args.week_start {
|
||||
new_config.week_start = val;
|
||||
}
|
||||
|
||||
if let Some(path) = config.path.as_deref() {
|
||||
let output = new_config.write(path)?;
|
||||
|
||||
writeln!(out, "Your new config:\n")?;
|
||||
|
||||
out.write_all(output.as_bytes())?;
|
||||
} else {
|
||||
writeln!(out, "Your config file is in memory and cannot be written to")?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,12 +2,14 @@ use std::env;
|
|||
use std::path::{Path, PathBuf};
|
||||
use std::fs::{File, create_dir_all};
|
||||
use std::io::{Read, Write};
|
||||
use std::str::FromStr;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use directories::{UserDirs, ProjectDirs};
|
||||
use serde::{Serialize, Deserialize};
|
||||
use toml::to_string;
|
||||
|
||||
use crate::{error::{Result, Error::*}, formatters::Formatter};
|
||||
use crate::{error::{Result, Error::{self, *}}, formatters::Formatter};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Copy, Clone)]
|
||||
pub enum WeekDay {
|
||||
|
@ -20,8 +22,31 @@ pub enum WeekDay {
|
|||
Sunday,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
impl FromStr for WeekDay {
|
||||
type Err = Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<WeekDay> {
|
||||
Ok(match s.to_lowercase().as_str() {
|
||||
"monday" => WeekDay::Monday,
|
||||
"tuesday" => WeekDay::Tuesday,
|
||||
"wednesday" => WeekDay::Wednesday,
|
||||
"thursday" => WeekDay::Thursday,
|
||||
"friday" => WeekDay::Friday,
|
||||
"saturday" => WeekDay::Saturday,
|
||||
"sunday" => WeekDay::Sunday,
|
||||
x => return Err(InvalidWeekDaySpec(x.to_owned())),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Config {
|
||||
#[serde(skip)]
|
||||
pub path: Option<PathBuf>,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub extra: HashMap<String, serde_json::Value>,
|
||||
|
||||
/// Absolute path to the sqlite database
|
||||
pub database_file: PathBuf, // "/home/user/.timetrap.db"
|
||||
|
||||
|
@ -119,6 +144,38 @@ impl Config {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn write<P: AsRef<Path>>(&self, path: P) -> Result<String> {
|
||||
let path = path.as_ref();
|
||||
let ext = path.extension().map(|e| e.to_str()).flatten();
|
||||
|
||||
let output = match ext {
|
||||
Some("toml") => {
|
||||
toml::to_string(self).unwrap()
|
||||
},
|
||||
Some("yaml" | "yml") => {
|
||||
serde_yaml::to_string(self).unwrap()
|
||||
},
|
||||
Some(ext) => {
|
||||
return Err(Error::GenericFailure(format!("\
|
||||
Your config file has '{}' extension which I don't understand, so I'd rather not
|
||||
mess with its contents. If its formatted as toml use '.toml' extension. If it is
|
||||
yaml use '.yml' or '.yaml'", ext)));
|
||||
},
|
||||
None => {
|
||||
return Err(Error::GenericFailure(format!("\
|
||||
Your config file, located at {} has no extension so I'll not write to it. Please
|
||||
set it an extension like '.toml' or '.yaml' (and ensure it matches the content's
|
||||
format)", path.display())));
|
||||
},
|
||||
};
|
||||
|
||||
let mut f = File::create(path)?;
|
||||
|
||||
f.write_all(output.as_bytes())?;
|
||||
|
||||
Ok(output)
|
||||
}
|
||||
|
||||
fn read_from_yaml<P: AsRef<Path>>(path: P) -> Result<Config> {
|
||||
let path: PathBuf = path.as_ref().into();
|
||||
|
||||
|
@ -133,9 +190,13 @@ impl Config {
|
|||
error: e,
|
||||
})?;
|
||||
|
||||
serde_yaml::from_str(&contents).map_err(|error| YamlError {
|
||||
path, error
|
||||
})
|
||||
let mut config: Config = serde_yaml::from_str(&contents).map_err(|error| YamlError {
|
||||
path: path.clone(), error
|
||||
})?;
|
||||
|
||||
config.path = Some(path);
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
fn read_from_toml<P: AsRef<Path>>(path: P) -> Result<Config> {
|
||||
|
@ -152,9 +213,13 @@ impl Config {
|
|||
error: e,
|
||||
})?;
|
||||
|
||||
toml::from_str(&contents).map_err(|error| TomlError {
|
||||
path, error
|
||||
})
|
||||
let mut config: Config = toml::from_str(&contents).map_err(|error| TomlError {
|
||||
path: path.clone(), error
|
||||
})?;
|
||||
|
||||
config.path = Some(path);
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// Assume the configuration file does not exist, create a default one and
|
||||
|
@ -177,6 +242,7 @@ impl Config {
|
|||
};
|
||||
|
||||
let config = Config {
|
||||
path: Some(config_filename.to_owned()),
|
||||
database_file: database_filename,
|
||||
formatter_search_paths: vec![formatter_search_paths],
|
||||
auto_sheet_search_paths: vec![auto_sheet_search_paths],
|
||||
|
@ -200,6 +266,9 @@ impl Config {
|
|||
impl Default for Config {
|
||||
fn default() -> Config {
|
||||
Config {
|
||||
path: None,
|
||||
extra: HashMap::new(),
|
||||
|
||||
database_file: PathBuf::new(), // see above for definition
|
||||
round_in_seconds: 900,
|
||||
append_notes_delimiter: " ".into(),
|
||||
|
|
|
@ -10,6 +10,12 @@ pub enum Error {
|
|||
#[error("The subcommand '{0}' is not implemented")]
|
||||
UnimplementedCommand(String),
|
||||
|
||||
/// Sometimes a specific variant for an error is not necessary if the error
|
||||
/// can only happen in one place in the code. This is that the generic error
|
||||
/// is for and nothing else.
|
||||
#[error("Error: {0}")]
|
||||
GenericFailure(String),
|
||||
|
||||
#[error("Sqlite error: {0}")]
|
||||
Sqlite(#[from] rusqlite::Error),
|
||||
|
||||
|
@ -97,6 +103,9 @@ query it using t backend or the sqlite3 command provided by your system.")]
|
|||
#[error("Could not understand '{0}' as a month specifier. Try 'this', 'last', or any month name like 'january' or 'nov'")]
|
||||
InvalidMonthSpec(String),
|
||||
|
||||
#[error("Could not understand '{0}' as a week day. Try 'monday' or 'TuesDay'")]
|
||||
InvalidWeekDaySpec(String),
|
||||
|
||||
#[error("An error ocurred while trying to read entries from the database.
|
||||
|
||||
In the row with id {id} the data at column '{col}' has a value that is not a
|
||||
|
|
|
@ -13,7 +13,7 @@ pub mod json;
|
|||
pub mod ids;
|
||||
pub mod ical;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum Formatter {
|
||||
Text,
|
||||
|
@ -57,9 +57,7 @@ impl FromStr for Formatter {
|
|||
type Err = error::Error;
|
||||
|
||||
fn from_str(s: &str) -> error::Result<Formatter> {
|
||||
let lower = s.to_lowercase();
|
||||
|
||||
Ok(match &*lower {
|
||||
Ok(match s.to_lowercase().as_str() {
|
||||
"text" => Formatter::Text,
|
||||
"csv" => Formatter::Csv,
|
||||
"json" => Formatter::Json,
|
||||
|
|
37
src/main.rs
37
src/main.rs
|
@ -15,6 +15,7 @@ use tiempo::commands::{
|
|||
month::MonthCommand, list::ListCommand, out::OutCommand,
|
||||
resume::ResumeCommand, backend::BackendCommand, kill::KillCommand,
|
||||
now::NowCommand, edit::EditCommand, archive::ArchiveCommand,
|
||||
configure::ConfigureCommand,
|
||||
};
|
||||
|
||||
fn error_trap(matches: ArgMatches) -> error::Result<()> {
|
||||
|
@ -46,6 +47,7 @@ fn error_trap(matches: ArgMatches) -> error::Result<()> {
|
|||
("now", Some(matches)) => NowCommand::handle(matches.try_into()?, &mut conn, &mut out, &mut err, &config, now),
|
||||
("edit", Some(matches)) => EditCommand::handle(matches.try_into()?, &mut conn, &mut out, &mut err, &config, now),
|
||||
("archive", Some(matches)) => ArchiveCommand::handle(matches.try_into()?, &mut conn, &mut out, &mut err, &config, now),
|
||||
("configure", Some(matches)) => ConfigureCommand::handle(matches.try_into()?, &mut conn, &mut out, &mut err, &config, now),
|
||||
|
||||
(cmd, _) => Err(error::Error::UnimplementedCommand(cmd.into())),
|
||||
}
|
||||
|
@ -94,15 +96,16 @@ fn main() {
|
|||
.help("Use this time instead of now");
|
||||
|
||||
let num_re = Regex::new(r"^\d+$").unwrap();
|
||||
let is_number = move |v: String| if num_re.is_match(&v) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("the --id arg must be a number. '{}' is not a valid number", v))
|
||||
};
|
||||
|
||||
let id_arg = Arg::with_name("id")
|
||||
.long("id")
|
||||
.takes_value(true).value_name("ID")
|
||||
.validator(move |v| if num_re.is_match(&v) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(format!("the --id arg must be a number. '{}' is not a valid number", v))
|
||||
});
|
||||
.validator(is_number);
|
||||
|
||||
// Now declar this app's cli
|
||||
let matches = App::new("Tiempo")
|
||||
|
@ -132,12 +135,12 @@ fn main() {
|
|||
|
||||
.subcommand(SubCommand::with_name("configure")
|
||||
.visible_alias("c")
|
||||
.about("Configure tiempo. Print path to config file.")
|
||||
.about("Configure tiempo in-place. If no arguments are given it just prints the path to the config file in use.")
|
||||
.arg(Arg::with_name("round_in_seconds")
|
||||
.long("round-in-seconds")
|
||||
.takes_value(true)
|
||||
.value_name("SECONDS")
|
||||
.help("The duration of time to use for rounding with the -r flag"))
|
||||
.help("The duration of time to use for rounding with the -r flag. Default: 900 (15 m)"))
|
||||
.arg(Arg::with_name("database_file")
|
||||
.long("database-file")
|
||||
.takes_value(true)
|
||||
|
@ -147,33 +150,41 @@ fn main() {
|
|||
.long("append-notes-delimiter")
|
||||
.takes_value(true)
|
||||
.value_name("DELIMITER")
|
||||
.help("delimiter used when appending notes via t edit --append"))
|
||||
.help("delimiter used when appending notes via t edit --append. Default: ' ' (space)"))
|
||||
.arg(Arg::with_name("formatter_search_paths")
|
||||
.long("formatter-search-paths")
|
||||
.takes_value(true)
|
||||
.multiple(true)
|
||||
.value_name("PATHS")
|
||||
.help("comma separated directories to search for user defined fomatter classes"))
|
||||
.arg(Arg::with_name("default_formatter")
|
||||
.long("default-formatter")
|
||||
.takes_value(true)
|
||||
.value_name("FORMATTER")
|
||||
.help("The format to use when display is invoked without a `--format` option"))
|
||||
.help("The format to use when display is invoked without a `--format` option. Default 'text'"))
|
||||
.arg(Arg::with_name("require_note")
|
||||
.long("require-note")
|
||||
.help("Prompt for a note if one isn't provided when checking in"))
|
||||
.help("Prompt for a note if one isn't provided when checking in (default)"))
|
||||
.arg(Arg::with_name("no_require_note")
|
||||
.long("no-require-note")
|
||||
.help("Prompt for a note if one isn't provided when checking in"))
|
||||
.help("Entries can be created without notes"))
|
||||
.arg(Arg::with_name("auto_checkout")
|
||||
.long("auto-checkout")
|
||||
.help("Checkout of current running entry when starting a new one"))
|
||||
.arg(Arg::with_name("no_auto_checkout")
|
||||
.long("no-auto-checkout")
|
||||
.help("Starting a new entry fails if one is running (default)"))
|
||||
.arg(Arg::with_name("note_editor")
|
||||
.long("note-editor")
|
||||
.takes_value(true)
|
||||
.value_name("EDITOR")
|
||||
.help("Command to launch notes editor or false if no editor use."))
|
||||
.help("Command to launch notes editor. Default: $EDITOR"))
|
||||
.arg(Arg::with_name("week_start")
|
||||
.long("week-start")
|
||||
.takes_value(true)
|
||||
.value_name("DAY")
|
||||
.help("The day of the week to use as the start of the week for t week."))
|
||||
.possible_values(&["monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"])
|
||||
.help("The day of the week to use as the start of the week for t week. Default: monday"))
|
||||
)
|
||||
|
||||
.subcommand(SubCommand::with_name("display")
|
||||
|
|
Loading…
Reference in New Issue