Merge pull request #228 from taskchampion/issue164-edit

Support editing config file from the command line
This commit is contained in:
Dustin J. Mitchell 2021-05-17 21:35:54 -04:00 committed by GitHub
commit fd4b35f3fe
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 600 additions and 227 deletions

46
Cargo.lock generated
View file

@ -324,6 +324,12 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b"
[[package]]
name = "ascii"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e"
[[package]]
name = "assert_cmd"
version = "1.0.3"
@ -573,6 +579,19 @@ dependencies = [
"vec_map",
]
[[package]]
name = "combine"
version = "3.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da3da6baa321ec19e1cc41d31bf599f00c783d0517095cdaf0332e3fe8d20680"
dependencies = [
"ascii",
"byteorder",
"either",
"memchr",
"unreachable",
]
[[package]]
name = "const_fn"
version = "0.4.6"
@ -2158,6 +2177,7 @@ dependencies = [
"termcolor",
"textwrap 0.13.4",
"toml",
"toml_edit",
]
[[package]]
@ -2404,6 +2424,17 @@ dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09391a441b373597cf0888d2b052dcf82c5be4fee05da3636ae30fb57aad8484"
dependencies = [
"chrono",
"combine",
"linked-hash-map",
]
[[package]]
name = "tracing"
version = "0.1.25"
@ -2522,6 +2553,15 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
[[package]]
name = "unreachable"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56"
dependencies = [
"void",
]
[[package]]
name = "untrusted"
version = "0.7.1"
@ -2578,6 +2618,12 @@ version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
[[package]]
name = "void"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d"
[[package]]
name = "wait-timeout"
version = "0.2.0"

View file

@ -15,6 +15,7 @@ textwrap = { version="^0.13.4", features=["terminal_size"] }
termcolor = "^1.1.2"
atty = "^0.2.14"
toml = "^0.5.8"
toml_edit = "^0.2.0"
[dependencies.taskchampion]
path = "../taskchampion"

View file

@ -0,0 +1,36 @@
use super::args::{any, arg_matching, literal};
use super::ArgList;
use crate::usage;
use nom::{combinator::*, sequence::*, IResult};
#[derive(Debug, PartialEq)]
/// A config operation
pub(crate) enum ConfigOperation {
/// Set a configuration value
Set(String, String),
}
impl ConfigOperation {
pub(super) fn parse(input: ArgList) -> IResult<ArgList, ConfigOperation> {
fn set_to_op(input: (&str, &str, &str)) -> Result<ConfigOperation, ()> {
Ok(ConfigOperation::Set(input.1.to_owned(), input.2.to_owned()))
}
map_res(
tuple((
arg_matching(literal("set")),
arg_matching(any),
arg_matching(any),
)),
set_to_op,
)(input)
}
pub(super) fn get_usage(u: &mut usage::Usage) {
u.subcommands.push(usage::Subcommand {
name: "config set",
syntax: "config set <key> <value>",
summary: "Set a configuration value",
description: "Update Taskchampion configuration file to set key = value",
});
}
}

View file

@ -18,12 +18,14 @@ That is, they contain no references, and have no methods to aid in their executi
*/
mod args;
mod command;
mod config;
mod filter;
mod modification;
mod subcommand;
pub(crate) use args::TaskId;
pub(crate) use command::Command;
pub(crate) use config::ConfigOperation;
pub(crate) use filter::{Condition, Filter};
pub(crate) use modification::{DescriptionMod, Modification};
pub(crate) use subcommand::Subcommand;

View file

@ -1,5 +1,5 @@
use super::args::*;
use super::{ArgList, DescriptionMod, Filter, Modification};
use super::{ArgList, ConfigOperation, DescriptionMod, Filter, Modification};
use crate::usage;
use nom::{branch::alt, combinator::*, sequence::*, IResult};
use taskchampion::Status;
@ -25,6 +25,11 @@ pub(crate) enum Subcommand {
summary: bool,
},
/// Manipulate configuration
Config {
config_operation: ConfigOperation,
},
/// Add a new task
Add {
modification: Modification,
@ -61,6 +66,7 @@ impl Subcommand {
all_consuming(alt((
Version::parse,
Help::parse,
Config::parse,
Add::parse,
Modify::parse,
Info::parse,
@ -74,6 +80,7 @@ impl Subcommand {
pub(super) fn get_usage(u: &mut usage::Usage) {
Version::get_usage(u);
Help::get_usage(u);
Config::get_usage(u);
Add::get_usage(u);
Modify::get_usage(u);
Info::get_usage(u);
@ -131,6 +138,26 @@ impl Help {
fn get_usage(_u: &mut usage::Usage) {}
}
struct Config;
impl Config {
fn parse(input: ArgList) -> IResult<ArgList, Subcommand> {
fn to_subcommand(input: (&str, ConfigOperation)) -> Result<Subcommand, ()> {
Ok(Subcommand::Config {
config_operation: input.1,
})
}
map_res(
tuple((arg_matching(literal("config")), ConfigOperation::parse)),
to_subcommand,
)(input)
}
fn get_usage(u: &mut usage::Usage) {
ConfigOperation::get_usage(u);
}
}
struct Add;
impl Add {
@ -427,6 +454,19 @@ mod test {
);
}
#[test]
fn test_config_set() {
assert_eq!(
Subcommand::parse(argv!["config", "set", "x", "y"]).unwrap(),
(
&EMPTY[..],
Subcommand::Config {
config_operation: ConfigOperation::Set("x".to_owned(), "y".to_owned())
}
)
);
}
#[test]
fn test_add_description() {
let subcommand = Subcommand::Add {

View file

@ -0,0 +1,62 @@
use crate::argparse::ConfigOperation;
use crate::settings::Settings;
use termcolor::{ColorSpec, WriteColor};
pub(crate) fn execute<W: WriteColor>(
w: &mut W,
config_operation: ConfigOperation,
settings: &Settings,
) -> anyhow::Result<()> {
match config_operation {
ConfigOperation::Set(key, value) => {
let filename = settings.set(&key, &value)?;
write!(w, "Set configuration value ")?;
w.set_color(ColorSpec::new().set_bold(true))?;
write!(w, "{}", &key)?;
w.set_color(ColorSpec::new().set_bold(false))?;
write!(w, " in ")?;
w.set_color(ColorSpec::new().set_bold(true))?;
writeln!(w, "{:?}.", filename)?;
w.set_color(ColorSpec::new().set_bold(false))?;
}
}
Ok(())
}
#[cfg(test)]
mod test {
use super::*;
use crate::invocation::test::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn test_config_set() {
let cfg_dir = TempDir::new().unwrap();
let cfg_file = cfg_dir.path().join("foo.toml");
fs::write(
cfg_file.clone(),
"# store data everywhere\ndata_dir = \"/nowhere\"\n",
)
.unwrap();
let settings = Settings::load_from_file(cfg_file.clone(), true).unwrap();
let mut w = test_writer();
execute(
&mut w,
ConfigOperation::Set("data_dir".to_owned(), "/somewhere".to_owned()),
&settings,
)
.unwrap();
assert!(w.into_string().starts_with("Set configuration value "));
let updated_toml = fs::read_to_string(cfg_file.clone()).unwrap();
dbg!(&updated_toml);
assert_eq!(
updated_toml,
"# store data everywhere\ndata_dir = \"/somewhere\"\n"
);
}
}

View file

@ -1,6 +1,7 @@
//! Responsible for executing commands as parsed by [`crate::argparse`].
pub(crate) mod add;
pub(crate) mod config;
pub(crate) mod gc;
pub(crate) mod help;
pub(crate) mod info;

View file

@ -35,6 +35,10 @@ pub(crate) fn invoke(command: Command, settings: Settings) -> anyhow::Result<()>
subcommand: Subcommand::Help { summary },
command_name,
} => return cmd::help::execute(&mut w, command_name, summary),
Command {
subcommand: Subcommand::Config { config_operation },
..
} => return cmd::config::execute(&mut w, config_operation, &settings),
Command {
subcommand: Subcommand::Version,
..
@ -90,6 +94,10 @@ pub(crate) fn invoke(command: Command, settings: Settings) -> anyhow::Result<()>
subcommand: Subcommand::Help { .. },
..
} => unreachable!(),
Command {
subcommand: Subcommand::Config { .. },
..
} => unreachable!(),
Command {
subcommand: Subcommand::Version,
..

View file

@ -1,5 +1,6 @@
#![deny(clippy::all)]
#![allow(clippy::unnecessary_wraps)] // for Rust 1.50, https://github.com/rust-lang/rust-clippy/pull/6765
#![allow(clippy::module_inception)] // we use re-exports to shorten stuttering paths like settings::settings::Settings
/*!
This crate implements the command-line interface to TaskChampion.

View file

@ -4,230 +4,8 @@
//! startup and not just when those values are used.
mod report;
mod settings;
mod util;
use crate::argparse::{Condition, Filter};
use anyhow::{anyhow, Context, Result};
use std::collections::HashMap;
use std::convert::TryFrom;
use std::env;
use std::fs;
use std::path::PathBuf;
use taskchampion::Status;
use toml::value::Table;
use util::table_with_keys;
pub(crate) use report::{Column, Property, Report, Sort, SortBy};
#[derive(Debug)]
pub(crate) struct Settings {
// replica
pub(crate) data_dir: PathBuf,
// remote sync server
pub(crate) server_client_key: Option<String>,
pub(crate) server_origin: Option<String>,
pub(crate) encryption_secret: Option<String>,
// local sync server
pub(crate) server_dir: PathBuf,
// reports
pub(crate) reports: HashMap<String, Report>,
}
impl Settings {
pub(crate) fn read() -> Result<Self> {
if let Some(config_file) = env::var_os("TASKCHAMPION_CONFIG") {
log::debug!("Loading configuration from {:?}", config_file);
env::remove_var("TASKCHAMPION_CONFIG");
Self::load_from_file(config_file.into(), true)
} else if let Some(mut dir) = dirs_next::config_dir() {
dir.push("taskchampion.toml");
log::debug!("Loading configuration from {:?} (optional)", dir);
Self::load_from_file(dir, false)
} else {
Ok(Default::default())
}
}
fn load_from_file(config_file: PathBuf, required: bool) -> Result<Self> {
let mut settings = Self::default();
let config_toml = match fs::read_to_string(config_file.clone()) {
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return if required {
Err(e.into())
} else {
Ok(settings)
};
}
Err(e) => return Err(e.into()),
Ok(s) => s,
};
let config_toml = config_toml
.parse::<toml::Value>()
.with_context(|| format!("error while reading {:?}", config_file))?;
settings
.update_from_toml(&config_toml)
.with_context(|| format!("error while parsing {:?}", config_file))?;
Ok(settings)
}
/// Update this object with configuration from the given config file. This is
/// broken out mostly for convenience in error handling
fn update_from_toml(&mut self, config_toml: &toml::Value) -> Result<()> {
let table_keys = [
"data_dir",
"server_client_key",
"server_origin",
"encryption_secret",
"server_dir",
"reports",
];
let table = table_with_keys(&config_toml, &table_keys)?;
fn get_str_cfg<F: FnOnce(String)>(
table: &Table,
name: &'static str,
setter: F,
) -> Result<()> {
if let Some(v) = table.get(name) {
setter(
v.as_str()
.ok_or_else(|| anyhow!(".{}: not a string", name))?
.to_owned(),
);
}
Ok(())
}
get_str_cfg(table, "data_dir", |v| {
self.data_dir = v.into();
})?;
get_str_cfg(table, "server_client_key", |v| {
self.server_client_key = Some(v);
})?;
get_str_cfg(table, "server_origin", |v| {
self.server_origin = Some(v);
})?;
get_str_cfg(table, "encryption_secret", |v| {
self.encryption_secret = Some(v);
})?;
get_str_cfg(table, "server_dir", |v| {
self.server_dir = v.into();
})?;
if let Some(v) = table.get("reports") {
let report_cfgs = v
.as_table()
.ok_or_else(|| anyhow!(".reports: not a table"))?;
for (name, cfg) in report_cfgs {
let report = Report::try_from(cfg).map_err(|e| anyhow!("reports.{}{}", name, e))?;
self.reports.insert(name.clone(), report);
}
}
Ok(())
}
}
impl Default for Settings {
fn default() -> Self {
let data_dir;
let server_dir;
if let Some(dir) = dirs_next::data_local_dir() {
data_dir = dir.join("taskchampion");
server_dir = dir.join("taskchampion-sync-server");
} else {
// fallback
data_dir = PathBuf::from(".");
server_dir = PathBuf::from(".");
}
// define the default reports
let mut reports = HashMap::new();
reports.insert(
"list".to_owned(),
Report {
sort: vec![Sort {
ascending: true,
sort_by: SortBy::Uuid,
}],
columns: vec![
Column {
label: "id".to_owned(),
property: Property::Id,
},
Column {
label: "description".to_owned(),
property: Property::Description,
},
Column {
label: "active".to_owned(),
property: Property::Active,
},
Column {
label: "tags".to_owned(),
property: Property::Tags,
},
],
filter: Default::default(),
},
);
reports.insert(
"next".to_owned(),
Report {
sort: vec![
Sort {
ascending: true,
sort_by: SortBy::Id,
},
Sort {
ascending: true,
sort_by: SortBy::Uuid,
},
],
columns: vec![
Column {
label: "id".to_owned(),
property: Property::Id,
},
Column {
label: "description".to_owned(),
property: Property::Description,
},
Column {
label: "active".to_owned(),
property: Property::Active,
},
Column {
label: "tags".to_owned(),
property: Property::Tags,
},
],
filter: Filter {
conditions: vec![Condition::Status(Status::Pending)],
},
},
);
Self {
data_dir,
server_client_key: None,
server_origin: None,
encryption_secret: None,
server_dir,
reports,
}
}
}
pub(crate) use settings::Settings;

View file

@ -0,0 +1,364 @@
use super::util::table_with_keys;
use super::{Column, Property, Report, Sort, SortBy};
use crate::argparse::{Condition, Filter};
use anyhow::{anyhow, bail, Context, Result};
use std::collections::HashMap;
use std::convert::TryFrom;
use std::env;
use std::fs;
use std::path::PathBuf;
use taskchampion::Status;
use toml::value::Table;
use toml_edit::Document;
#[derive(Debug, PartialEq)]
pub(crate) struct Settings {
// filename from which this configuration was loaded, if any
pub(crate) filename: Option<PathBuf>,
// replica
pub(crate) data_dir: PathBuf,
// remote sync server
pub(crate) server_client_key: Option<String>,
pub(crate) server_origin: Option<String>,
pub(crate) encryption_secret: Option<String>,
// local sync server
pub(crate) server_dir: PathBuf,
// reports
pub(crate) reports: HashMap<String, Report>,
}
impl Settings {
pub(crate) fn read() -> Result<Self> {
if let Some(config_file) = env::var_os("TASKCHAMPION_CONFIG") {
log::debug!("Loading configuration from {:?}", config_file);
env::remove_var("TASKCHAMPION_CONFIG");
Self::load_from_file(config_file.into(), true)
} else if let Some(filename) = Settings::default_filename() {
log::debug!("Loading configuration from {:?} (optional)", filename);
Self::load_from_file(filename, false)
} else {
Ok(Default::default())
}
}
/// Get the default filename for the configuration, or None if that cannot
/// be determined.
fn default_filename() -> Option<PathBuf> {
if let Some(dir) = dirs_next::config_dir() {
Some(dir.join("taskchampion.toml"))
} else {
None
}
}
/// Update this settings object with the contents of the given TOML file. Top-level settings
/// are overwritten, and reports are overwritten by name.
pub(crate) fn load_from_file(config_file: PathBuf, required: bool) -> Result<Self> {
let mut settings = Self::default();
let config_toml = match fs::read_to_string(config_file.clone()) {
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return if required {
Err(e.into())
} else {
settings.filename = Some(config_file);
Ok(settings)
};
}
Err(e) => return Err(e.into()),
Ok(s) => s,
};
let config_toml = config_toml
.parse::<toml::Value>()
.with_context(|| format!("error while reading {:?}", config_file))?;
settings.filename = Some(config_file.clone());
settings
.update_from_toml(&config_toml)
.with_context(|| format!("error while parsing {:?}", config_file))?;
Ok(settings)
}
/// Update this object with configuration from the given config file. This is
/// broken out mostly for convenience in error handling
fn update_from_toml(&mut self, config_toml: &toml::Value) -> Result<()> {
let table_keys = [
"data_dir",
"server_client_key",
"server_origin",
"encryption_secret",
"server_dir",
"reports",
];
let table = table_with_keys(&config_toml, &table_keys)?;
fn get_str_cfg<F: FnOnce(String)>(
table: &Table,
name: &'static str,
setter: F,
) -> Result<()> {
if let Some(v) = table.get(name) {
setter(
v.as_str()
.ok_or_else(|| anyhow!(".{}: not a string", name))?
.to_owned(),
);
}
Ok(())
}
get_str_cfg(table, "data_dir", |v| {
self.data_dir = v.into();
})?;
get_str_cfg(table, "server_client_key", |v| {
self.server_client_key = Some(v);
})?;
get_str_cfg(table, "server_origin", |v| {
self.server_origin = Some(v);
})?;
get_str_cfg(table, "encryption_secret", |v| {
self.encryption_secret = Some(v);
})?;
get_str_cfg(table, "server_dir", |v| {
self.server_dir = v.into();
})?;
if let Some(v) = table.get("reports") {
let report_cfgs = v
.as_table()
.ok_or_else(|| anyhow!(".reports: not a table"))?;
for (name, cfg) in report_cfgs {
let report = Report::try_from(cfg).map_err(|e| anyhow!("reports.{}{}", name, e))?;
self.reports.insert(name.clone(), report);
}
}
Ok(())
}
/// Set a value in the config file, modifying it in place. Returns the filename.
pub(crate) fn set(&self, key: &str, value: &str) -> Result<PathBuf> {
let allowed_keys = [
"data_dir",
"server_client_key",
"server_origin",
"encryption_secret",
"server_dir",
// reports is not allowed, since it is not a string
];
if !allowed_keys.contains(&key) {
bail!("No such configuration key {}", key);
}
let filename = if let Some(ref f) = self.filename {
f.clone()
} else {
Settings::default_filename()
.ok_or_else(|| anyhow!("Could not determine config file name"))?
};
let mut document = fs::read_to_string(filename.clone())
.context("Could not read existing configuration file")?
.parse::<Document>()
.context("Could not parse existing configuration file")?;
document[key] = toml_edit::value(value);
fs::write(filename.clone(), document.to_string())
.context("Could not write updated configuration file")?;
Ok(filename)
}
}
impl Default for Settings {
fn default() -> Self {
let data_dir;
let server_dir;
if let Some(dir) = dirs_next::data_local_dir() {
data_dir = dir.join("taskchampion");
server_dir = dir.join("taskchampion-sync-server");
} else {
// fallback
data_dir = PathBuf::from(".");
server_dir = PathBuf::from(".");
}
// define the default reports
let mut reports = HashMap::new();
reports.insert(
"list".to_owned(),
Report {
sort: vec![Sort {
ascending: true,
sort_by: SortBy::Uuid,
}],
columns: vec![
Column {
label: "id".to_owned(),
property: Property::Id,
},
Column {
label: "description".to_owned(),
property: Property::Description,
},
Column {
label: "active".to_owned(),
property: Property::Active,
},
Column {
label: "tags".to_owned(),
property: Property::Tags,
},
],
filter: Default::default(),
},
);
reports.insert(
"next".to_owned(),
Report {
sort: vec![
Sort {
ascending: true,
sort_by: SortBy::Id,
},
Sort {
ascending: true,
sort_by: SortBy::Uuid,
},
],
columns: vec![
Column {
label: "id".to_owned(),
property: Property::Id,
},
Column {
label: "description".to_owned(),
property: Property::Description,
},
Column {
label: "active".to_owned(),
property: Property::Active,
},
Column {
label: "tags".to_owned(),
property: Property::Tags,
},
],
filter: Filter {
conditions: vec![Condition::Status(Status::Pending)],
},
},
);
Self {
filename: None,
data_dir,
server_client_key: None,
server_origin: None,
encryption_secret: None,
server_dir,
reports,
}
}
}
#[cfg(test)]
mod test {
use super::*;
use tempfile::TempDir;
use toml::toml;
#[test]
fn test_load_from_file_not_required() {
let cfg_dir = TempDir::new().unwrap();
let cfg_file = cfg_dir.path().join("foo.toml");
let settings = Settings::load_from_file(cfg_file.clone(), false).unwrap();
let mut expected = Settings::default();
expected.filename = Some(cfg_file.clone());
assert_eq!(settings, expected);
}
#[test]
fn test_load_from_file_required() {
let cfg_dir = TempDir::new().unwrap();
assert!(Settings::load_from_file(cfg_dir.path().join("foo.toml"), true).is_err());
}
#[test]
fn test_load_from_file_exists() {
let cfg_dir = TempDir::new().unwrap();
let cfg_file = cfg_dir.path().join("foo.toml");
fs::write(cfg_file.clone(), "data_dir = \"/nowhere\"").unwrap();
let settings = Settings::load_from_file(cfg_file.clone(), true).unwrap();
assert_eq!(settings.data_dir, PathBuf::from("/nowhere"));
assert_eq!(settings.filename, Some(cfg_file));
}
#[test]
fn test_update_from_toml_top_level_keys() {
let val = toml! {
data_dir = "/data"
server_client_key = "sck"
server_origin = "so"
encryption_secret = "es"
server_dir = "/server"
};
let mut settings = Settings::default();
settings.update_from_toml(&val).unwrap();
assert_eq!(settings.data_dir, PathBuf::from("/data"));
assert_eq!(settings.server_client_key, Some("sck".to_owned()));
assert_eq!(settings.server_origin, Some("so".to_owned()));
assert_eq!(settings.encryption_secret, Some("es".to_owned()));
assert_eq!(settings.server_dir, PathBuf::from("/server"));
}
#[test]
fn test_update_from_toml_report() {
let val = toml! {
[reports.foo]
sort = [ { sort_by = "id" } ]
columns = [ { label = "ID", property = "id" } ]
};
let mut settings = Settings::default();
settings.update_from_toml(&val).unwrap();
assert!(settings.reports.get("foo").is_some());
// beyond existence of this report, we can rely on Report's unit tests
}
#[test]
fn test_set_valid_key() {
let cfg_dir = TempDir::new().unwrap();
let cfg_file = cfg_dir.path().join("foo.toml");
fs::write(cfg_file.clone(), "server_dir = \"/srv\"").unwrap();
let settings = Settings::load_from_file(cfg_file.clone(), true).unwrap();
assert_eq!(settings.filename, Some(cfg_file.clone()));
settings.set("data_dir", "/data").unwrap();
// load the file again and see the change
let settings = Settings::load_from_file(cfg_file.clone(), true).unwrap();
assert_eq!(settings.data_dir, PathBuf::from("/data"));
assert_eq!(settings.server_dir, PathBuf::from("/srv"));
assert_eq!(settings.filename, Some(cfg_file));
}
}

View file

@ -40,7 +40,15 @@ If using a remote server:
* `server_client_key` - Client key to identify this replica to the sync server (a UUID)
If not set, then sync is done to a local server.
# Reports
## Reports
* `reports` - a mapping of each report's name to its definition.
See [Reports](./reports.md) for details.
## Editing
As a shortcut, the simple, top-level configuration values can be edited from the command line:
```shell
ta config set data_dir /home/myuser/.taskchampion
```

View file

@ -11,13 +11,39 @@ Synchronization is quick, especially if no changes have occurred.
Each replica expects to be synchronized frequently, even if no server is involved.
Without periodic syncs, the storage space used for the task database will grow quickly, and performance will suffer.
## Local Sync
By default, TaskChampion syncs to a "local server", as specified by the `server_dir` configuration parameter.
This defaults to `taskchampion-sync-server` in your [data directory](https://docs.rs/dirs-next/2.0.0/dirs_next/fn.data_dir.html), but can be customized in the configuration file.
## Remote Sync
For remote synchronization, you will need a few pieces of information.
From the server operator, you will need an origin and a client key.
Configure these with
```shell
ta config set server_origin "<origin from server operator>"
ta config set server_client_key "<client key from server operator>"
```
You will need to generate your own encryption secret.
This is used to encrypt your task history, so treat it as a password.
The following will use the `openssl` utility to generate a suitable value:
```shell
ta config set encryption_secret $(openssl rand -hex 35)
```
Every replica sharing a task history should have precisely the same configuration for `server_origin`, `server_client_key`, and `encryption_secret`.
### Adding a New Replica
Synchronizing a new replica to an existing task history is easy: begin with an empty replica, configured for the remote server, and run `ta sync`.
The replica will download the entire task history.
### Upgrading a Locally-Sync'd Replica
It is possible to switch a single replica to a remote server by simply configuring for the remote server and running `ta sync`.
The replica will upload the entire task history to the server.
Once this is complete, additional replicas can be configured with the same settings in order to share the task history.