Add configuration-file support to the 'task' command

This commit is contained in:
Dustin J. Mitchell 2020-11-28 18:18:43 -05:00
parent 8af7ba286d
commit 0e926df578
6 changed files with 236 additions and 45 deletions

178
Cargo.lock generated
View file

@ -74,7 +74,7 @@ dependencies = [
"pin-project 1.0.2",
"rand 0.7.3",
"regex",
"serde",
"serde 1.0.117",
"serde_json",
"serde_urlencoded",
"sha-1",
@ -102,7 +102,7 @@ dependencies = [
"http",
"log",
"regex",
"serde",
"serde 1.0.117",
]
[[package]]
@ -241,7 +241,7 @@ dependencies = [
"mime",
"pin-project 1.0.2",
"regex",
"serde",
"serde 1.0.117",
"serde_json",
"serde_urlencoded",
"socket2",
@ -372,7 +372,7 @@ dependencies = [
"mime",
"percent-encoding",
"rand 0.7.3",
"serde",
"serde 1.0.117",
"serde_json",
"serde_urlencoded",
]
@ -479,7 +479,7 @@ dependencies = [
"lazy_static",
"memchr",
"regex-automata",
"serde",
"serde 1.0.117",
]
[[package]]
@ -535,8 +535,8 @@ checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73"
dependencies = [
"libc",
"num-integer",
"num-traits",
"serde",
"num-traits 0.2.14",
"serde 1.0.117",
"time 0.1.44",
"winapi 0.3.9",
]
@ -580,6 +580,22 @@ 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",
"rust-ini",
"serde 1.0.117",
"serde-hjson",
"serde_json",
"toml",
"yaml-rust",
]
[[package]]
name = "const_fn"
version = "0.4.3"
@ -613,7 +629,7 @@ dependencies = [
"idna",
"log",
"publicsuffix",
"serde",
"serde 1.0.117",
"serde_json",
"time 0.2.23",
"url",
@ -661,7 +677,7 @@ dependencies = [
"csv-core",
"itoa",
"ryu",
"serde",
"serde 1.0.117",
]
[[package]]
@ -710,6 +726,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"
@ -804,7 +840,7 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1267f4ac4f343772758f7b1bdcbe767c218bbab93bb432acbf5162bbf85a6c4"
dependencies = [
"num-traits",
"num-traits 0.2.14",
]
[[package]]
@ -1132,7 +1168,7 @@ checksum = "cb79e59d356a5ae85b13990bbb3649a293d64df1ca6e7890822076186527a9f7"
dependencies = [
"lmdb-rkv",
"rmp-serde",
"serde",
"serde 1.0.117",
"thiserror",
"toml",
]
@ -1149,12 +1185,35 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d58d1b70b004888f764dfbf6a26a3b0342a1632d33968e4a179d8011c760614"
[[package]]
name = "linked-hash-map"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d262045c5b87c0861b3f004610afd0e2c851e2908d08b6c870cbb9d5f494ecd"
dependencies = [
"serde 0.8.23",
"serde_test",
]
[[package]]
name = "linked-hash-map"
version = "0.5.3"
@ -1208,7 +1267,7 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c"
dependencies = [
"linked-hash-map",
"linked-hash-map 0.5.3",
]
[[package]]
@ -1298,6 +1357,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"
@ -1311,7 +1381,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db"
dependencies = [
"autocfg 1.0.1",
"num-traits",
"num-traits 0.2.14",
]
[[package]]
name = "num-traits"
version = "0.1.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92e5113e9fd4cc14ded8e499429f396a20f98c772a47cc8622a736e1ec843c31"
dependencies = [
"num-traits 0.2.14",
]
[[package]]
@ -1527,7 +1606,7 @@ dependencies = [
"bitflags",
"byteorder",
"lazy_static",
"num-traits",
"num-traits 0.2.14",
"quick-error",
"rand 0.6.5",
"rand_chacha 0.1.1",
@ -1828,7 +1907,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f10b46df14cf1ee1ac7baa4d2fbc2c52c0622a4b82fa8740e37bc452ac0184f"
dependencies = [
"byteorder",
"num-traits",
"num-traits 0.2.14",
]
[[package]]
@ -1839,7 +1918,7 @@ checksum = "4ce7d70c926fe472aed493b902010bccc17fa9f7284145cb8772fd22fdb052d8"
dependencies = [
"byteorder",
"rmp",
"serde",
"serde 1.0.117",
]
[[package]]
@ -1854,6 +1933,12 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "rust-ini"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e52c148ef37f8c375d49d5a73aa70713125b7f19095948a923f80afdeb22ec2"
[[package]]
name = "rustc-demangle"
version = "0.1.18"
@ -1931,6 +2016,12 @@ version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3"
[[package]]
name = "serde"
version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9dad3f759919b92c3068c696c15c3d17238234498bbdcc80f2c469606f948ac8"
[[package]]
name = "serde"
version = "1.0.117"
@ -1940,6 +2031,19 @@ dependencies = [
"serde_derive",
]
[[package]]
name = "serde-hjson"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a3a4e0ea8a88553209f6cc6cfe8724ecad22e1acf372793c27d995290fe74f8"
dependencies = [
"lazy_static",
"linked-hash-map 0.3.0",
"num-traits 0.1.43",
"regex",
"serde 0.8.23",
]
[[package]]
name = "serde_derive"
version = "1.0.117"
@ -1959,7 +2063,16 @@ checksum = "dcac07dbffa1c65e7f816ab9eba78eb142c6d44410f4eeba1e26e4f5dfa56b95"
dependencies = [
"itoa",
"ryu",
"serde",
"serde 1.0.117",
]
[[package]]
name = "serde_test"
version = "0.8.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "110b3dbdf8607ec493c22d5d947753282f3bae73c0f56d322af1e8c78e4c23d5"
dependencies = [
"serde 0.8.23",
]
[[package]]
@ -1971,7 +2084,7 @@ dependencies = [
"form_urlencoded",
"itoa",
"ryu",
"serde",
"serde 1.0.117",
]
[[package]]
@ -2041,6 +2154,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"
@ -2063,7 +2182,7 @@ checksum = "c87a60a40fccc84bef0652345bbbbbe20a605bf5d0ce81719fc476f5c03b50ef"
dependencies = [
"proc-macro2",
"quote",
"serde",
"serde 1.0.117",
"serde_derive",
"syn",
]
@ -2077,7 +2196,7 @@ dependencies = [
"base-x",
"proc-macro2",
"quote",
"serde",
"serde 1.0.117",
"serde_derive",
"serde_json",
"sha1",
@ -2128,7 +2247,7 @@ dependencies = [
"kv",
"lmdb-rkv",
"proptest",
"serde",
"serde 1.0.117",
"serde_json",
"tempdir",
"ureq",
@ -2141,6 +2260,8 @@ version = "0.1.0"
dependencies = [
"assert_cmd",
"clap",
"config",
"dirs 3.0.1",
"failure",
"predicates",
"prettytable-rs",
@ -2189,7 +2310,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edd106a334b7657c10b7c540a0106114feadeb4dc314513e97df481d5d966f42"
dependencies = [
"byteorder",
"dirs",
"dirs 1.0.5",
"winapi 0.3.9",
]
@ -2344,7 +2465,7 @@ version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75cf45bb0bef80604d001caaec0d09da99611b3c0fd39d3080468875cdb65645"
dependencies = [
"serde",
"serde 1.0.117",
]
[[package]]
@ -2510,7 +2631,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fde2f6a4bea1d6e007c4ad38c6839fa71cbb63b6dbf5b595aa38dc9b1093c11"
dependencies = [
"rand 0.7.3",
"serde",
"serde 1.0.117",
]
[[package]]
@ -2687,3 +2808,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 0.5.3",
]

View file

@ -9,6 +9,8 @@ clap = "^2.33.0"
taskchampion = { path = "../taskchampion" }
failure = "^0.1.8"
prettytable-rs = "^0.8.0"
config = "^0.10.1"
dirs = "^3.0.1"
[dev-dependencies]
assert_cmd = "^1.0.1"

View file

@ -1,7 +1,8 @@
use crate::settings;
use clap::Arg;
use config::{Config, ConfigError};
use failure::{format_err, Fallible};
use std::env;
use std::ffi::OsString;
use std::cell::{Ref, RefCell};
use taskchampion::{server, Replica, ReplicaConfig, ServerConfig, Task, Uuid};
pub(super) fn task_arg<'a>() -> Arg<'a, 'a> {
@ -33,11 +34,15 @@ pub(super) fn get_task<S: AsRef<str>>(replica: &mut Replica, task_arg: S) -> Fal
#[derive(Debug)]
pub struct CommandInvocation {
pub(crate) subcommand: Box<dyn super::SubCommandInvocation>,
settings: RefCell<Config>,
}
impl CommandInvocation {
pub(crate) fn new(subcommand: Box<dyn super::SubCommandInvocation>) -> Self {
Self { subcommand }
Self {
subcommand,
settings: RefCell::new(Config::default()),
}
}
pub fn run(self) -> Fallible<()> {
@ -46,28 +51,33 @@ impl CommandInvocation {
// -- utilities for command invocations
pub(super) fn get_settings(&self) -> Fallible<Ref<Config>> {
{
// 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_replica(&self) -> Fallible<Replica> {
// temporarily use $TASK_DB to locate the taskdb
let taskdb_dir = env::var_os("TASK_DB").unwrap_or_else(|| OsString::from("/tmp/tasks"));
let settings = self.get_settings()?;
let replica_config = ReplicaConfig {
taskdb_dir: taskdb_dir.into(),
taskdb_dir: settings.get_str("data_dir")?.into(),
};
Ok(Replica::from_config(replica_config)?)
}
pub(super) fn get_server(&self) -> Fallible<Box<dyn server::Server>> {
// temporarily use $SYNC_SERVER_ORIGIN for the sync server
let origin = env::var_os("SYNC_SERVER_ORIGIN")
.map(|osstr| osstr.into_string().unwrap())
.unwrap_or_else(|| String::from("http://localhost:8080"));
let client_id = env::var_os("SYNC_SERVER_CLIENT_ID")
.ok_or_else(|| format_err!("SYNC_SERVER_CLIENT_ID not set"))?;
let client_id = client_id
.into_string()
.map_err(|_| format_err!("SYNC_SERVER_CLIENT_ID is not a valid UUID"))?;
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,
origin: settings.get_str("server_origin")?,
client_id,
})?)
}

View file

@ -3,6 +3,7 @@ use failure::Fallible;
use std::ffi::OsString;
mod cmd;
pub(crate) mod settings;
mod table;
use cmd::ArgMatchResult;

35
cli/src/settings.rs Normal file
View file

@ -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<Config> {
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<FileSourceFile> = 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<FileSourceFile> = dir.into();
settings.merge(config_file.required(false))?;
}
// merge environment variables
settings.merge(Environment::with_prefix("TASKCHAMPION"))?;
Ok(settings)
}

View file

@ -9,13 +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 client ID to use with the sync server is givne by `SYNC_SERVER_CLIENT_ID` (with no default).
The `task` command will work out-of-the-box with no configuration file, using default values.
Configuration is read from `taskchampion.yaml` (or `taskchampion.toml` or `taskchmapion.json` if you prefer) 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.