Add first pass on ideas for generating the config.

pull/3/head
Joshua Potter 2021-12-29 22:38:04 -05:00
parent 00dbfab415
commit b4369739ac
5 changed files with 141 additions and 44 deletions

View File

@ -10,6 +10,7 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
ansi_term = "0.12.1"
clap = { version = "3.0.0-rc.9", features = ["derive"] } clap = { version = "3.0.0-rc.9", features = ["derive"] }
notify = "4.0.16" notify = "4.0.16"
serde = "1.0" serde = "1.0"

View File

@ -1,6 +1,7 @@
---
remote: remote:
owner: jrpotter owner: test
name: home-config name: woah
packages: packages:
homesync: homesync:
configs: configs:

View File

@ -1,11 +1,9 @@
use ansi_term::Colour::Green;
use serde_derive::{Deserialize, Serialize}; use serde_derive::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
use std::env; use std::io::Write;
use std::error; use std::path::{Path, PathBuf};
use std::fmt; use std::{env, error, fmt, fs, io};
use std::fs;
use std::io;
use std::path::PathBuf;
// ======================================== // ========================================
// Error // Error
@ -69,10 +67,28 @@ impl Config {
pub fn new(contents: &str) -> Result<Self> { pub fn new(contents: &str) -> Result<Self> {
Ok(serde_yaml::from_str(&contents)?) Ok(serde_yaml::from_str(&contents)?)
} }
pub fn default() -> Self {
Config {
remote: Remote {
owner: "example-user".to_owned(),
name: "home-config".to_owned(),
},
packages: HashMap::new(),
}
}
pub fn save(&self, path: &Path) -> Result<()> {
// TODO(jrpotter): Create backup file before overwriting.
let mut file = fs::File::create(path)?;
let serialized = serde_yaml::to_string(&self)?;
file.write_all(serialized.as_bytes())?;
Ok(())
}
} }
// ======================================== // ========================================
// Public // Loading
// ======================================== // ========================================
/// Returns the default configuration files `homesync` looks for. /// Returns the default configuration files `homesync` looks for.
@ -83,7 +99,7 @@ impl Config {
/// - `$XDG_CONFIG_HOME/homesync/homesync.yml` /// - `$XDG_CONFIG_HOME/homesync/homesync.yml`
/// ///
/// Returned `PathBuf`s are looked for in the above order. /// Returned `PathBuf`s are looked for in the above order.
pub fn default_configs() -> Vec<PathBuf> { pub fn default_paths() -> Vec<PathBuf> {
let mut paths: Vec<PathBuf> = Vec::new(); let mut paths: Vec<PathBuf> = Vec::new();
if let Ok(home) = env::var("HOME") { if let Ok(home) = env::var("HOME") {
paths.extend_from_slice(&[ paths.extend_from_slice(&[
@ -104,15 +120,50 @@ pub fn default_configs() -> Vec<PathBuf> {
paths paths
} }
pub fn read_config(paths: &Vec<PathBuf>) -> Result<Config> { pub fn load(paths: &Vec<PathBuf>) -> Result<(&Path, Config)> {
// When trying our paths, the only acceptable error is a `NotFound` file. // When trying our paths, the only acceptable error is a `NotFound` file.
// Anything else should be surfaced to the end user. // Anything else should be surfaced to the end user.
for path in paths { for path in paths {
match fs::read_to_string(path) { match fs::read_to_string(path) {
Err(err) if err.kind() == io::ErrorKind::NotFound => continue, Err(err) if err.kind() == io::ErrorKind::NotFound => continue,
Err(err) => return Err(Error::FileError(err)), Err(err) => return Err(Error::FileError(err)),
Ok(contents) => return Ok(Config::new(&contents)?), Ok(contents) => return Ok((&path, Config::new(&contents)?)),
} }
} }
Err(Error::MissingConfig) Err(Error::MissingConfig)
} }
// ========================================
// Initialization
// ========================================
pub fn init(path: &Path, default: Config) -> Result<()> {
// TODO(jrpotter): Use curses to make this nicer.
println!(
"Generating config at {}...\n\n",
Green.paint(path.display().to_string())
);
print!(
"Git repository owner <{}> (enter to continue): ",
default.remote.owner
);
io::stdout().flush()?;
let mut owner = String::new();
io::stdin().read_line(&mut owner)?;
let owner = owner.trim().to_owned();
print!(
"Git repository name <{}> (enter to continue): ",
default.remote.name
);
io::stdout().flush()?;
let mut name = String::new();
io::stdin().read_line(&mut name)?;
let name = name.trim().to_owned();
Config {
remote: Remote { owner, name },
packages: default.packages,
}
.save(path)
}

View File

@ -1,36 +1,63 @@
pub mod config; pub mod config;
use ansi_term::Colour::Green;
use config::Config;
use std::error::Error; use std::error::Error;
use std::path::PathBuf; use std::path::{Path, PathBuf};
pub fn run_configure( pub fn run_add(paths: Vec<PathBuf>) -> Result<(), config::Error> {
paths: Vec<PathBuf>, debug_assert!(!paths.is_empty(), "`run_init` paths empty");
_matches: &clap::ArgMatches, if paths.is_empty() {
) -> Result<(), Box<dyn Error>> { return Err(config::Error::MissingConfig);
}
// TODO(jrpotter): Show $EDITOR that allows writing specific package.
Ok(())
}
pub fn run_init(paths: Vec<PathBuf>) -> Result<(), config::Error> {
// TODO(jrpotter): Use a nonempty implementation instead of this.
debug_assert!(!paths.is_empty(), "`run_init` paths empty");
if paths.is_empty() {
return Err(config::Error::MissingConfig);
}
// Check if we already have a local config somewhere. If so, reprompt the // Check if we already have a local config somewhere. If so, reprompt the
// same configuration options and override the values present in the current // same configuration options and override the values present in the current
// YAML file. // YAML file.
match config::read_config(&paths) { match config::load(&paths) {
Ok(_) => { Ok((path, config)) => config::init(path, config),
print!("successfully read\n"); // TODO(jrpotter): Verify I have permission to write at specified path.
Ok(()) // Make directories if necessary.
} Err(config::Error::MissingConfig) => config::init(&paths[0], Config::default()),
Err(config::Error::MissingConfig) => { Err(e) => Err(e),
print!("missing config\n");
Ok(())
}
Err(e) => Err(Box::new(e)),
} }
} }
pub fn run_push(_matches: &clap::ArgMatches) -> Result<(), Box<dyn Error>> { pub fn run_list(paths: Vec<PathBuf>) -> Result<(), config::Error> {
debug_assert!(!paths.is_empty(), "`run_init` paths empty");
if paths.is_empty() {
return Err(config::Error::MissingConfig);
}
match config::load(&paths) {
Ok((path, config)) => {
// TODO(jrpotter): Should sort these entries.
// Also clean up where I use the console writing or not.
println!(
"Listing packages at {}...\n",
Green.paint(path.display().to_string())
);
for (k, _) in config.packages {
println!("{}", k);
}
Ok(())
}
Err(e) => Err(e),
}
}
pub fn run_pull(_: &clap::ArgMatches) -> Result<(), Box<dyn Error>> {
Ok(()) Ok(())
} }
pub fn run_pull(_matches: &clap::ArgMatches) -> Result<(), Box<dyn Error>> { pub fn run_push(_: &clap::ArgMatches) -> Result<(), Box<dyn Error>> {
Ok(())
}
pub fn run_add(_matches: &clap::ArgMatches) -> Result<(), Box<dyn Error>> {
Ok(()) Ok(())
} }

View File

@ -1,8 +1,7 @@
use clap::{App, AppSettings, Arg}; use clap::{App, AppSettings, Arg};
use std::error::Error;
use std::path::PathBuf; use std::path::PathBuf;
fn main() -> Result<(), Box<dyn Error>> { fn main() {
let matches = App::new("homesync") let matches = App::new("homesync")
.about("Cross desktop configuration sync tool.") .about("Cross desktop configuration sync tool.")
.version("0.1.0") .version("0.1.0")
@ -16,22 +15,40 @@ fn main() -> Result<(), Box<dyn Error>> {
.help("Specify a configuration file to use in place of defaults") .help("Specify a configuration file to use in place of defaults")
.takes_value(true), .takes_value(true),
) )
.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")) .subcommand(App::new("add").about("Add new configuration to local repository"))
.subcommand(App::new("init").about("Initialize the homesync local repository"))
.subcommand(App::new("list").about("See which packages homesync manages"))
.subcommand(App::new("pull").about("Pull remote repository into local repository"))
.subcommand(App::new("push").about("Push local repository to remote repository"))
.get_matches(); .get_matches();
let configs = match matches.value_of("config") { let paths = match matches.value_of("config") {
Some(path) => vec![PathBuf::from(path)], Some(path) => vec![PathBuf::from(path)],
None => homesync::config::default_configs(), None => homesync::config::default_paths(),
}; };
match matches.subcommand() { match matches.subcommand() {
Some(("configure", ms)) => homesync::run_configure(configs, ms), Some(("add", _)) => {
Some(("push", ms)) => homesync::run_push(ms), if let Err(e) = homesync::run_add(paths) {
Some(("pull", ms)) => homesync::run_pull(ms), eprintln!("{}", e);
Some(("add", ms)) => homesync::run_add(ms), }
}
Some(("init", _)) => {
if let Err(e) = homesync::run_init(paths) {
eprintln!("{}", e);
}
}
Some(("list", _)) => {
if let Err(e) = homesync::run_list(paths) {
eprintln!("{}", e);
}
}
Some(("pull", ms)) => {
homesync::run_pull(ms);
}
Some(("push", ms)) => {
homesync::run_push(ms);
}
_ => unreachable!(), _ => unreachable!(),
} }
} }