diff --git a/Cargo.lock b/Cargo.lock index 4147eb7c9..6b649fe16 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -580,6 +580,18 @@ dependencies = [ "bitflags", ] +[[package]] +name = "config" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b076e143e1d9538dde65da30f8481c2a6c44040edb8e02b9bf1351edb92ce3" +dependencies = [ + "lazy_static", + "nom", + "serde", + "yaml-rust", +] + [[package]] name = "const_fn" version = "0.4.3" @@ -710,6 +722,26 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "dirs" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142995ed02755914747cc6ca76fc7e4583cd18578746716d0508ea6ed558b9ff" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e93d7f5705de3e49895a2b5e0b8855a1c27f080192ae9c32a6432d50741a57a" +dependencies = [ + "libc", + "redox_users", + "winapi 0.3.9", +] + [[package]] name = "discard" version = "1.0.4" @@ -1149,6 +1181,19 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +[[package]] +name = "lexical-core" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db65c6da02e61f55dae90a0ae427b2a5f6b3e8db09f58d10efab23af92592616" +dependencies = [ + "arrayvec", + "bitflags", + "cfg-if 0.1.10", + "ryu", + "static_assertions", +] + [[package]] name = "libc" version = "0.2.80" @@ -1298,6 +1343,17 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "nom" +version = "5.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af" +dependencies = [ + "lexical-core", + "memchr", + "version_check", +] + [[package]] name = "normalize-line-endings" version = "0.3.0" @@ -2041,6 +2097,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "stdweb" version = "0.4.20" @@ -2141,6 +2203,8 @@ version = "0.1.0" dependencies = [ "assert_cmd", "clap", + "config", + "dirs 3.0.1", "failure", "predicates", "prettytable-rs", @@ -2189,7 +2253,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "edd106a334b7657c10b7c540a0106114feadeb4dc314513e97df481d5d966f42" dependencies = [ "byteorder", - "dirs", + "dirs 1.0.5", "winapi 0.3.9", ] @@ -2687,3 +2751,12 @@ dependencies = [ "winapi 0.2.8", "winapi-build", ] + +[[package]] +name = "yaml-rust" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39f0c922f1a334134dc2f7a8b67dc5d25f0735263feec974345ff706bcf20b0d" +dependencies = [ + "linked-hash-map", +] diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 89b1b9ed9..36e313f8e 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -9,6 +9,8 @@ clap = "^2.33.0" taskchampion = { path = "../taskchampion" } failure = "^0.1.8" prettytable-rs = "^0.8.0" +config = { version="^0.10.1", default-features=false, features=["yaml"] } +dirs = "^3.0.1" [dev-dependencies] assert_cmd = "^1.0.1" diff --git a/cli/src/cmd/add.rs b/cli/src/cmd/add.rs index cfc26b5a8..88e7a76f5 100644 --- a/cli/src/cmd/add.rs +++ b/cli/src/cmd/add.rs @@ -38,7 +38,7 @@ define_subcommand! { subcommand_invocation! { fn run(&self, command: &CommandInvocation) -> Fallible<()> { let t = command - .get_replica() + .get_replica()? .new_task(Status::Pending, self.description.clone()) .unwrap(); println!("added task {}", t.get_uuid()); diff --git a/cli/src/cmd/gc.rs b/cli/src/cmd/gc.rs index d8948a715..15bdd288b 100644 --- a/cli/src/cmd/gc.rs +++ b/cli/src/cmd/gc.rs @@ -20,7 +20,7 @@ define_subcommand! { subcommand_invocation! { fn run(&self, command: &CommandInvocation) -> Fallible<()> { - command.get_replica().gc()?; + command.get_replica()?.gc()?; println!("garbage collected."); Ok(()) } diff --git a/cli/src/cmd/info.rs b/cli/src/cmd/info.rs index cdc39cc11..f55667dfb 100644 --- a/cli/src/cmd/info.rs +++ b/cli/src/cmd/info.rs @@ -30,7 +30,7 @@ define_subcommand! { subcommand_invocation! { fn run(&self, command: &CommandInvocation) -> Fallible<()> { - let mut replica = command.get_replica(); + let mut replica = command.get_replica()?; let task = shared::get_task(&mut replica, &self.task)?; let uuid = task.get_uuid(); diff --git a/cli/src/cmd/list.rs b/cli/src/cmd/list.rs index d29227c92..72ee206e0 100644 --- a/cli/src/cmd/list.rs +++ b/cli/src/cmd/list.rs @@ -23,7 +23,7 @@ define_subcommand! { subcommand_invocation! { fn run(&self, command: &CommandInvocation) -> Fallible<()> { - let mut replica = command.get_replica(); + let mut replica = command.get_replica()?; let mut t = Table::new(); t.set_format(table::format()); t.set_titles(row![b->"id", b->"description"]); diff --git a/cli/src/cmd/modify.rs b/cli/src/cmd/modify.rs index 487084a53..662941e34 100644 --- a/cli/src/cmd/modify.rs +++ b/cli/src/cmd/modify.rs @@ -36,7 +36,7 @@ define_subcommand! { subcommand_invocation! { fn run(&self, command: &CommandInvocation) -> Fallible<()> { - let mut replica = command.get_replica(); + let mut replica = command.get_replica()?; let task = shared::get_task(&mut replica, &self.task)?; let mut task = task.into_mut(&mut replica); diff --git a/cli/src/cmd/pending.rs b/cli/src/cmd/pending.rs index b55c044d0..f35756b8b 100644 --- a/cli/src/cmd/pending.rs +++ b/cli/src/cmd/pending.rs @@ -25,7 +25,7 @@ define_subcommand! { subcommand_invocation! { fn run(&self, command: &CommandInvocation) -> Fallible<()> { - let working_set = command.get_replica().working_set().unwrap(); + let working_set = command.get_replica()?.working_set().unwrap(); let mut t = Table::new(); t.set_format(table::format()); t.set_titles(row![b->"id", b->"description"]); diff --git a/cli/src/cmd/shared.rs b/cli/src/cmd/shared.rs index 474bca280..9744ea6f3 100644 --- a/cli/src/cmd/shared.rs +++ b/cli/src/cmd/shared.rs @@ -1,8 +1,9 @@ +use crate::settings; use clap::Arg; +use config::{Config, ConfigError}; use failure::{format_err, Fallible}; -use std::env; -use std::ffi::OsString; -use taskchampion::{server, taskstorage, Replica, Task, Uuid}; +use std::cell::{Ref, RefCell}; +use taskchampion::{server, Replica, ReplicaConfig, ServerConfig, Task, Uuid}; pub(super) fn task_arg<'a>() -> Arg<'a, 'a> { Arg::with_name("task") @@ -33,11 +34,15 @@ pub(super) fn get_task>(replica: &mut Replica, task_arg: S) -> Fal #[derive(Debug)] pub struct CommandInvocation { pub(crate) subcommand: Box, + settings: RefCell, } impl CommandInvocation { pub(crate) fn new(subcommand: Box) -> Self { - Self { subcommand } + Self { + subcommand, + settings: RefCell::new(Config::default()), + } } pub fn run(self) -> Fallible<()> { @@ -46,20 +51,34 @@ impl CommandInvocation { // -- utilities for command invocations - pub(super) fn get_replica(&self) -> Replica { - // temporarily use $TASK_DB to locate the taskdb - let taskdb_dir = env::var_os("TASK_DB").unwrap_or_else(|| OsString::from("/tmp/tasks")); - Replica::new(Box::new(taskstorage::KVStorage::new(taskdb_dir).unwrap())) + pub(super) fn get_settings(&self) -> Fallible> { + { + // use the special `_loaded" config value to detect whether we have + // loaded the configuration yet + let mut settings = self.settings.borrow_mut(); + if let Err(ConfigError::NotFound(_)) = settings.get_bool("_loaded") { + settings.merge(settings::read_settings()?)?; + settings.set("_loaded", true)?; + } + } + Ok(self.settings.borrow()) } - pub(super) fn get_server(&self) -> Fallible { - // temporarily use $SYNC_SERVER_ORIGIN for the sync server - let sync_server_origin = env::var_os("SYNC_SERVER_ORIGIN") - .map(|osstr| osstr.into_string().unwrap()) - .unwrap_or_else(|| String::from("http://localhost:8080")); - Ok(server::RemoteServer::new( - sync_server_origin, - Uuid::parse_str("d5b55cbd-9a82-4860-9a39-41b67893b22f").unwrap(), - )) + pub(super) fn get_replica(&self) -> Fallible { + let settings = self.get_settings()?; + let replica_config = ReplicaConfig { + taskdb_dir: settings.get_str("data_dir")?.into(), + }; + Ok(Replica::from_config(replica_config)?) + } + + pub(super) fn get_server(&self) -> Fallible> { + let settings = self.get_settings()?; + let client_id = settings.get_str("server_client_id")?; + let client_id = Uuid::parse_str(&client_id)?; + Ok(server::from_config(ServerConfig::Remote { + origin: settings.get_str("server_origin")?, + client_id, + })?) } } diff --git a/cli/src/cmd/sync.rs b/cli/src/cmd/sync.rs index a17920436..37c3a76d6 100644 --- a/cli/src/cmd/sync.rs +++ b/cli/src/cmd/sync.rs @@ -21,7 +21,7 @@ define_subcommand! { subcommand_invocation! { fn run(&self, command: &CommandInvocation) -> Fallible<()> { - let mut replica = command.get_replica(); + let mut replica = command.get_replica()?; let mut server = command.get_server()?; replica.sync(&mut server)?; Ok(()) diff --git a/cli/src/lib.rs b/cli/src/lib.rs index 48421f25a..4474af8f3 100644 --- a/cli/src/lib.rs +++ b/cli/src/lib.rs @@ -3,6 +3,7 @@ use failure::Fallible; use std::ffi::OsString; mod cmd; +pub(crate) mod settings; mod table; use cmd::ArgMatchResult; diff --git a/cli/src/settings.rs b/cli/src/settings.rs new file mode 100644 index 000000000..f8a92582e --- /dev/null +++ b/cli/src/settings.rs @@ -0,0 +1,35 @@ +use config::{Config, Environment, File, FileSourceFile}; +use failure::Fallible; +use std::env; +use std::path::PathBuf; + +pub(crate) fn read_settings() -> Fallible { + let mut settings = Config::default(); + + // set up defaults + if let Some(mut dir) = dirs::data_local_dir() { + dir.push("taskchampion"); + settings.set_default( + "data_dir", + // the config crate does not support non-string paths + dir.to_str().expect("data_local_dir is not utf-8"), + )?; + } + + // load either from the path in TASKCHAMPION_CONFIG, or from CONFIG_DIR/taskchampion + if let Some(config_file) = env::var_os("TASKCHAMPION_CONFIG") { + let config_file: PathBuf = config_file.into(); + let config_file: File = config_file.into(); + settings.merge(config_file.required(true))?; + env::remove_var("TASKCHAMPION_CONFIG"); + } else if let Some(mut dir) = dirs::config_dir() { + dir.push("taskchampion"); + let config_file: File = dir.into(); + settings.merge(config_file.required(false))?; + } + + // merge environment variables + settings.merge(Environment::with_prefix("TASKCHAMPION"))?; + + Ok(settings) +} diff --git a/docs/src/usage.md b/docs/src/usage.md index b16c724e1..37123bfd2 100644 --- a/docs/src/usage.md +++ b/docs/src/usage.md @@ -9,12 +9,26 @@ Note that the `task` interface does not match that of TaskWarrior. ### Configuration -Temporarily, configuration is by environment variables. -The directory containing the replica's task data is given by `TASK_DB`, defaulting to `/tmp/tasks`. -the origin of the sync server is given by `SYNC_SERVER_ORIGIN`, defaulting to `http://localhost:8080`. +The `task` command will work out-of-the-box with no configuration file, using default values. + +Configuration is read from `taskchampion.yaml` in your config directory. +On Linux systems, that directory is `~/.config`. +On OS X, it's `~/Library/Preferences`. +On Windows, it's `AppData/Roaming` in your home directory. +The path can be overridden by setting `$TASKCHAMPION_CONFIG`. + +Individual configuration parameters can be overridden by environemnt variables, converted to upper-case and prefixed with `TASKCHAMPION_`, e.g., `TASKCHAMPION_DATA_DIR`. +Nested configuration parameters cannot be overridden by environment variables. + +The following configuration parameters are available: + +* `data_dir` - path to a directory containing the replica's task data (which will be created if necessary). + Default: `taskchampion` in the local data directory +* `server_origin` - Origin of the taskchampion sync server, e.g., `https://taskchampion.example.com` +* `server_client_id` - Client ID to identify this replica to the sync server (a UUID) ## `taskchampion-sync-server` Run `taskchampion-sync-server` to start the sync server. It serves on port 8080 on all interfaces, using an in-memory database (meaning that all data is lost when the process exits). -Requests for previously-unknown clients are automatically added. +Requests for previously-unknown clients automatically create the client. diff --git a/taskchampion/src/config.rs b/taskchampion/src/config.rs new file mode 100644 index 000000000..8a8ee5bcd --- /dev/null +++ b/taskchampion/src/config.rs @@ -0,0 +1,26 @@ +use std::path::PathBuf; +use uuid::Uuid; + +/// The configuration required for a replica. Use with [`crate::Replica::from_config`]. +pub struct ReplicaConfig { + /// Path containing the task DB. + pub taskdb_dir: PathBuf, +} + +/// The configuration for a replica's access to a sync server. Use with +/// [`crate::server::from_config`]. +pub enum ServerConfig { + /// A local task database, for situations with a single replica. + Local { + /// Path containing the server's DB + server_dir: PathBuf, + }, + /// A remote taskchampion-sync-server instance + Remote { + /// Sync server "origin"; a URL with schema and hostname but no path or trailing `/` + origin: String, + + /// Client ID to identify this replica to the server + client_id: Uuid, + }, +} diff --git a/taskchampion/src/lib.rs b/taskchampion/src/lib.rs index 932dbdeef..aa2c8bf99 100644 --- a/taskchampion/src/lib.rs +++ b/taskchampion/src/lib.rs @@ -23,6 +23,7 @@ for more information about the design and usage of the tool. */ +mod config; mod errors; mod replica; pub mod server; @@ -31,6 +32,7 @@ mod taskdb; pub mod taskstorage; mod utils; +pub use config::{ReplicaConfig, ServerConfig}; pub use replica::Replica; pub use task::Priority; pub use task::Status; diff --git a/taskchampion/src/replica.rs b/taskchampion/src/replica.rs index c33d31028..525be9888 100644 --- a/taskchampion/src/replica.rs +++ b/taskchampion/src/replica.rs @@ -1,8 +1,9 @@ +use crate::config::ReplicaConfig; use crate::errors::Error; use crate::server::Server; use crate::task::{Status, Task}; use crate::taskdb::TaskDB; -use crate::taskstorage::{Operation, TaskMap, TaskStorage}; +use crate::taskstorage::{KVStorage, Operation, TaskMap, TaskStorage}; use chrono::Utc; use failure::Fallible; use std::collections::HashMap; @@ -21,6 +22,11 @@ impl Replica { } } + pub fn from_config(config: ReplicaConfig) -> Fallible { + let storage = Box::new(KVStorage::new(config.taskdb_dir)?); + Ok(Replica::new(storage)) + } + #[cfg(test)] pub fn new_inmemory() -> Replica { Replica::new(Box::new(crate::taskstorage::InMemoryStorage::new())) @@ -140,7 +146,7 @@ impl Replica { } /// Synchronize this replica against the given server. - pub fn sync(&mut self, server: &mut dyn Server) -> Fallible<()> { + pub fn sync(&mut self, server: &mut Box) -> Fallible<()> { self.taskdb.sync(server) } diff --git a/taskchampion/src/server/local.rs b/taskchampion/src/server/local.rs index 89775acf1..9cc4b2b28 100644 --- a/taskchampion/src/server/local.rs +++ b/taskchampion/src/server/local.rs @@ -25,7 +25,7 @@ pub struct LocalServer<'t> { impl<'t> LocalServer<'t> { /// A test server has no notion of clients, signatures, encryption, etc. - pub fn new(directory: &Path) -> Fallible { + pub fn new>(directory: P) -> Fallible> { let mut config = Config::default(directory); config.bucket("versions", None); config.bucket("numbers", None); diff --git a/taskchampion/src/server/mod.rs b/taskchampion/src/server/mod.rs index 78c49c6f6..341428130 100644 --- a/taskchampion/src/server/mod.rs +++ b/taskchampion/src/server/mod.rs @@ -1,3 +1,6 @@ +use crate::ServerConfig; +use failure::Fallible; + #[cfg(test)] pub(crate) mod test; @@ -8,3 +11,12 @@ mod types; pub use local::LocalServer; pub use remote::RemoteServer; pub use types::*; + +pub fn from_config(config: ServerConfig) -> Fallible> { + Ok(match config { + ServerConfig::Local { server_dir } => Box::new(LocalServer::new(server_dir)?), + ServerConfig::Remote { origin, client_id } => { + Box::new(RemoteServer::new(origin, client_id)) + } + }) +} diff --git a/taskchampion/src/taskdb.rs b/taskchampion/src/taskdb.rs index eb69e35ae..4cdb927ca 100644 --- a/taskchampion/src/taskdb.rs +++ b/taskchampion/src/taskdb.rs @@ -164,7 +164,7 @@ impl TaskDB { } /// Sync to the given server, pulling remote changes and pushing local changes. - pub fn sync(&mut self, server: &mut dyn Server) -> Fallible<()> { + pub fn sync(&mut self, server: &mut Box) -> Fallible<()> { let mut txn = self.storage.txn()?; // retry synchronizing until the server accepts our version (this allows for races between @@ -542,7 +542,7 @@ mod tests { #[test] fn test_sync() { - let mut server = TestServer::new(); + let mut server: Box = Box::new(TestServer::new()); let mut db1 = newdb(); db1.sync(&mut server).unwrap(); @@ -602,7 +602,7 @@ mod tests { #[test] fn test_sync_create_delete() { - let mut server = TestServer::new(); + let mut server: Box = Box::new(TestServer::new()); let mut db1 = newdb(); db1.sync(&mut server).unwrap(); @@ -692,7 +692,7 @@ mod tests { // and delete operations that results in a task existing in one TaskDB but not existing in // another. So, the generated sequences focus on a single task UUID. fn transform_sequences_of_operations(action_sequence in action_sequence_strategy()) { - let mut server = TestServer::new(); + let mut server: Box = Box::new(TestServer::new()); let mut dbs = [newdb(), newdb(), newdb()]; for (action, db) in action_sequence { diff --git a/taskchampion/src/taskstorage/mod.rs b/taskchampion/src/taskstorage/mod.rs index 4ddd7df75..b5f94c8ff 100644 --- a/taskchampion/src/taskstorage/mod.rs +++ b/taskchampion/src/taskstorage/mod.rs @@ -2,11 +2,13 @@ use failure::Fallible; use std::collections::HashMap; use uuid::Uuid; +#[cfg(test)] mod inmemory; mod kv; mod operation; pub use self::kv::KVStorage; +#[cfg(test)] pub use inmemory::InMemoryStorage; pub use operation::Operation;