Idea on how `configure` might work.
parent
071c461074
commit
8f3acba5e9
|
@ -12,4 +12,4 @@ edition = "2021"
|
|||
[dependencies]
|
||||
clap = { version = "3.0.0-rc.9", features = ["derive"] }
|
||||
notify = "4.0.16"
|
||||
yaml-rust = "0.4"
|
||||
yaml-rust = "0.4.4"
|
||||
|
|
12
README.md
12
README.md
|
@ -18,16 +18,16 @@ TODO
|
|||
## Configuration
|
||||
|
||||
Homesync uses a YAML file, to be found in anyone of the following locations.
|
||||
Locations are searched in the following order:
|
||||
Locations are searched in the following priority:
|
||||
|
||||
- `$XDG_CONFIG_HOME/homesync/homesync.yml`
|
||||
- `$XDG_CONFIG_HOME/homesync.yml`
|
||||
- `$HOME/.config/homesync/homesync.yml`
|
||||
- `$HOME/.homesync.yml`
|
||||
- `$HOME/.config/homesync/homesync.yml`
|
||||
- `$XDG_CONFIG_HOME/homesync.yml`
|
||||
- `$XDG_CONFIG_HOME/homesync/homesync.yml`
|
||||
|
||||
That said, it is recommended to modify this config solely from the exposed
|
||||
homesync CLI. Homesync will take responsibility ensuring how the config is
|
||||
modified based on your package manager, platform, etc.
|
||||
homesync CLI. Homesync will take responsibility ensuring the generated
|
||||
configuration is according to package manager, platform, etc.
|
||||
|
||||
## Usage
|
||||
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
remote:
|
||||
system:
|
||||
owner: jrpotter
|
||||
name: home-config
|
||||
packages:
|
||||
homesync:
|
||||
configs:
|
||||
- $XDG_CONFIG_HOME/homesync/homesync.yml
|
||||
- $XDG_CONFIG_HOME/homesync.yml
|
||||
- $HOME/.config/homesync/homesync.yml
|
||||
- $HOME/.homesync.yml
|
||||
- $HOME/.config/homesync/homesync.yml
|
||||
- $XDG_CONFIG_HOME/homesync.yml
|
||||
- $XDG_CONFIG_HOME/homesync/homesync.yml
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
pub mod config;
|
|
@ -0,0 +1,259 @@
|
|||
use std::env;
|
||||
use std::error;
|
||||
use std::fmt;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::path::{Path, PathBuf};
|
||||
use yaml_rust::{ScanError, Yaml, YamlLoader};
|
||||
|
||||
// ========================================
|
||||
// Error
|
||||
// ========================================
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Key {
|
||||
Packages, // OPTIONAL
|
||||
Remote, // REQUIRED
|
||||
RemoteName, // REQUIRED
|
||||
RemoteOwner, // REQUIRED
|
||||
}
|
||||
|
||||
impl Key {
|
||||
fn to_yaml(&self) -> Yaml {
|
||||
Yaml::String(self.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Key {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Key::Packages => write!(f, "packages"),
|
||||
Key::Remote => write!(f, "remote"),
|
||||
Key::RemoteName => write!(f, "name"),
|
||||
Key::RemoteOwner => write!(f, "owner"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ErrorKind {
|
||||
// Indicates our top-level data structure isn't a dictionary.
|
||||
InvalidHash,
|
||||
// Indicates a required key was not found.
|
||||
MissingKey(Key),
|
||||
// Indicates multiple YAML documents were found within our file.
|
||||
MultipleDocuments,
|
||||
// Indicates no YAML documents were found within our file.
|
||||
NoDocument,
|
||||
// Indicates there was a scan error when parsing the YAML.
|
||||
ScanError(ScanError),
|
||||
}
|
||||
|
||||
impl fmt::Display for ErrorKind {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
ErrorKind::InvalidHash => write!(f, "expected dictionary"),
|
||||
ErrorKind::MissingKey(k) => write!(f, "missing key '{}'", k),
|
||||
ErrorKind::MultipleDocuments => write!(f, "has multiple YAML documents"),
|
||||
ErrorKind::NoDocument => write!(f, "has no YAML document"),
|
||||
ErrorKind::ScanError(s) => s.fmt(f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ErrorWithFile {
|
||||
path: PathBuf,
|
||||
kind: ErrorKind,
|
||||
}
|
||||
|
||||
impl ErrorWithFile {
|
||||
fn new(path: &Path, kind: ErrorKind) -> Self {
|
||||
ErrorWithFile {
|
||||
path: path.to_path_buf(),
|
||||
kind,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ErrorWithFile {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let ErrorWithFile { path, kind } = self;
|
||||
write!(
|
||||
f,
|
||||
"File {} failed with error: {}",
|
||||
path.to_str().ok_or(fmt::Error)?,
|
||||
kind
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Error {
|
||||
// Indicates we could not find the configuration file at all.
|
||||
MissingConfig,
|
||||
// Indicates an error occurred when reading the configuration file.
|
||||
WithFile(ErrorWithFile),
|
||||
}
|
||||
|
||||
impl Error {
|
||||
pub fn new(path: &Path, kind: ErrorKind) -> Self {
|
||||
Error::WithFile(ErrorWithFile::new(path, kind))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
Error::MissingConfig => write!(
|
||||
f,
|
||||
"\
|
||||
Could not find a valid configuration file. Looked in \
|
||||
\n\n- `$HOME/.homesync.yml` \
|
||||
\n- `$HOME/.config/homesync/homesync.yml` \
|
||||
\n- `$XDG_CONFIG_HOME/homesync.yml` \
|
||||
\n- `$XDG_CONFIG_HOME/homesync/homesync.yml` \
|
||||
\nin order."
|
||||
),
|
||||
Error::WithFile(e) => write!(f, "{}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl error::Error for Error {}
|
||||
|
||||
// ========================================
|
||||
// Config
|
||||
// ========================================
|
||||
|
||||
pub struct Remote {
|
||||
pub owner: String,
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
pub struct Package {
|
||||
pub name: String,
|
||||
pub configs: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
pub struct Config {
|
||||
pub path: PathBuf,
|
||||
pub remote: Remote,
|
||||
pub packages: Vec<Package>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn new(path: &Path, contents: &str) -> Result<Self> {
|
||||
if let Yaml::Hash(pairs) = get_document(path, contents)? {
|
||||
let remote = pairs
|
||||
.get(&Key::Remote.to_yaml())
|
||||
.ok_or(Error::new(path, ErrorKind::MissingKey(Key::Remote)))?;
|
||||
let remote = parseRemote(path, remote)?;
|
||||
let packages = pairs.get(&Key::Packages.to_yaml()).unwrap_or(&Yaml::Null);
|
||||
let packages = parsePackages(path, packages)?;
|
||||
// We intentionally ignore any other keys we may encounter.
|
||||
Ok(Config {
|
||||
path: path.to_path_buf(),
|
||||
remote,
|
||||
packages,
|
||||
})
|
||||
} else {
|
||||
Err(Error::new(path, ErrorKind::InvalidHash))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_document(path: &Path, contents: &str) -> Result<Yaml> {
|
||||
match YamlLoader::load_from_str(contents) {
|
||||
Ok(mut docs) => {
|
||||
if docs.len() > 1 {
|
||||
Err(Error::new(path, ErrorKind::MultipleDocuments))
|
||||
} else if docs.is_empty() {
|
||||
Err(Error::new(path, ErrorKind::NoDocument))
|
||||
} else {
|
||||
Ok(docs.swap_remove(0))
|
||||
}
|
||||
}
|
||||
Err(e) => Err(Error::new(path, ErrorKind::ScanError(e))),
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Parsers
|
||||
// ========================================
|
||||
|
||||
fn parseRemote(path: &Path, value: &Yaml) -> Result<Remote> {
|
||||
Ok(Remote {
|
||||
owner: String::new(),
|
||||
name: String::new(),
|
||||
})
|
||||
}
|
||||
|
||||
fn parsePackages(path: &Path, value: &Yaml) -> Result<Vec<Package>> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// Public
|
||||
// ========================================
|
||||
|
||||
/// Attempt to read in the project config in the following priorities:
|
||||
///
|
||||
/// - `$HOME/.homesync.yml`
|
||||
/// - `$HOME/.config/homesync/homesync.yml`
|
||||
/// - `$XDG_CONFIG_HOME/homesync.yml`
|
||||
/// - `$XDG_CONFIG_HOME/homesync/homesync.yml`
|
||||
///
|
||||
/// Returns an error if a file does not exist in any of these locations or a
|
||||
/// found file contains invalid YAML.
|
||||
pub fn find_config() -> Result<Config> {
|
||||
let mut paths: Vec<PathBuf> = Vec::new();
|
||||
if let Ok(home) = env::var("HOME") {
|
||||
paths.extend_from_slice(&[
|
||||
[&home, ".homesync.yml"].iter().collect(),
|
||||
[&home, ".config", "homesync", "homesync.yml"]
|
||||
.iter()
|
||||
.collect(),
|
||||
]);
|
||||
}
|
||||
if let Ok(xdg_config_home) = env::var("XDG_CONFIG_HOME") {
|
||||
paths.extend_from_slice(&[
|
||||
[&xdg_config_home, "homesync.yml"].iter().collect(),
|
||||
[&xdg_config_home, "homesync", "homesync.yml"]
|
||||
.iter()
|
||||
.collect(),
|
||||
]);
|
||||
}
|
||||
// When trying our paths, the only acceptable error is a `NotFound` file.
|
||||
// Anything else should be surfaced to the end user.
|
||||
for path in paths {
|
||||
if let Ok(Some(contents)) = read_optional_config(&path) {
|
||||
return Ok(Config::new(&path, &contents)?);
|
||||
}
|
||||
}
|
||||
Err(Error::MissingConfig)
|
||||
}
|
||||
|
||||
fn read_optional_config(path: &Path) -> io::Result<Option<String>> {
|
||||
match fs::read_to_string(path) {
|
||||
Err(err) => match err.kind() {
|
||||
// Ignore `NotFound` since we want to try multiple paths.
|
||||
io::ErrorKind::NotFound => Ok(None),
|
||||
_ => Err(err),
|
||||
},
|
||||
Ok(contents) => Ok(Some(contents)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generate_config() -> Config {
|
||||
Config {
|
||||
path: PathBuf::from(""),
|
||||
remote: Remote {
|
||||
owner: "".to_owned(),
|
||||
name: "".to_owned(),
|
||||
},
|
||||
packages: Vec::new(),
|
||||
}
|
||||
}
|
75
src/lib.rs
75
src/lib.rs
|
@ -1,57 +1,28 @@
|
|||
use std::env;
|
||||
mod homesync;
|
||||
|
||||
use homesync::config;
|
||||
use std::error::Error;
|
||||
use std::fs;
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use yaml_rust::{Yaml, YamlLoader};
|
||||
|
||||
fn read_config(path: &PathBuf) -> io::Result<Option<String>> {
|
||||
match fs::read_to_string(path) {
|
||||
Err(err) => match err.kind() {
|
||||
// Ignore not found since we may try multiple paths.
|
||||
io::ErrorKind::NotFound => Ok(None),
|
||||
_ => Err(err),
|
||||
},
|
||||
Ok(contents) => Ok(Some(contents)),
|
||||
}
|
||||
}
|
||||
|
||||
fn find_config() -> Result<Vec<Yaml>, Box<dyn Error>> {
|
||||
let mut paths: Vec<PathBuf> = Vec::new();
|
||||
if let Ok(xdg_config_home) = env::var("XDG_CONFIG_HOME") {
|
||||
paths.push(
|
||||
[&xdg_config_home, "homesync", "homesync.yml"]
|
||||
.iter()
|
||||
.collect(),
|
||||
);
|
||||
paths.push([&xdg_config_home, "homesync.yml"].iter().collect());
|
||||
}
|
||||
if let Ok(home) = env::var("HOME") {
|
||||
paths.push(
|
||||
[&home, ".config", "homesync", "homesync.yml"]
|
||||
.iter()
|
||||
.collect(),
|
||||
);
|
||||
paths.push([&home, ".homesync.yml"].iter().collect());
|
||||
}
|
||||
for path in paths {
|
||||
if let Ok(Some(contents)) = read_config(&path) {
|
||||
return Ok(YamlLoader::load_from_str(&contents)?);
|
||||
}
|
||||
}
|
||||
Err(Box::new(io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
"Could not find a homesync config.",
|
||||
)))
|
||||
}
|
||||
|
||||
pub fn run(config: Option<String>) -> Result<(), Box<dyn Error>> {
|
||||
let _loaded = match config {
|
||||
Some(path) => {
|
||||
let contents = fs::read_to_string(path)?;
|
||||
YamlLoader::load_from_str(&contents)?
|
||||
}
|
||||
None => find_config()?,
|
||||
pub fn run_configure(_matches: &clap::ArgMatches) -> Result<(), Box<dyn Error>> {
|
||||
// Check if we already have a local config somewhere. If so, reprompt the
|
||||
// same configuration options and override the values present in the current
|
||||
// YAML file.
|
||||
let _config = match config::find_config() {
|
||||
Ok(conf) => Ok(conf),
|
||||
Err(config::Error::MissingConfig) => Ok(config::generate_config()),
|
||||
Err(config::Error::WithFile(e)) => Err(e),
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn run_push(_matches: &clap::ArgMatches) -> Result<(), Box<dyn Error>> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn run_pull(_matches: &clap::ArgMatches) -> Result<(), Box<dyn Error>> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn run_add(_matches: &clap::ArgMatches) -> Result<(), Box<dyn Error>> {
|
||||
Ok(())
|
||||
}
|
||||
|
|
33
src/main.rs
33
src/main.rs
|
@ -1,17 +1,22 @@
|
|||
use clap::Parser;
|
||||
use std::process;
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[clap(about, version, author)]
|
||||
struct Args {
|
||||
#[clap(short, long)]
|
||||
config: Option<String>,
|
||||
}
|
||||
use clap::{App, AppSettings};
|
||||
|
||||
fn main() {
|
||||
let args = Args::parse();
|
||||
homesync::run(args.config).unwrap_or_else(|err| {
|
||||
eprintln!("Problem parsing arguments: {}", err);
|
||||
process::exit(1);
|
||||
});
|
||||
let matches = App::new("homesync")
|
||||
.about("Cross desktop configuration sync tool.")
|
||||
.version("0.1.0")
|
||||
.setting(AppSettings::SubcommandRequiredElseHelp)
|
||||
.author("Joshua Potter <jrpotter.github.io>")
|
||||
.subcommand(App::new("configure").about("Initialize the homesync local repository."))
|
||||
.subcommand(App::new("push").about("Push local repository to remote repository."))
|
||||
.subcommand(App::new("pull").about("Pull remote repository into local repository."))
|
||||
.subcommand(App::new("add").about("Add new configuration to local repository."))
|
||||
.get_matches();
|
||||
|
||||
match matches.subcommand() {
|
||||
Some(("configure", ms)) => homesync::run_configure(ms).unwrap(),
|
||||
Some(("push", ms)) => homesync::run_push(ms).unwrap(),
|
||||
Some(("pull", ms)) => homesync::run_pull(ms).unwrap(),
|
||||
Some(("add", ms)) => homesync::run_add(ms).unwrap(),
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue