diff --git a/examples/config.yaml b/examples/config.yaml index 3395ff9..547e913 100644 --- a/examples/config.yaml +++ b/examples/config.yaml @@ -1,7 +1,7 @@ --- remote: - owner: test - name: woah + owner: jrpotter + name: home-config packages: homesync: configs: diff --git a/flake.nix b/flake.nix index 9d8a8fa..1d1f397 100644 --- a/flake.nix +++ b/flake.nix @@ -15,9 +15,11 @@ buildInputs = [ cargo rls + libiconv rustc rustfmt - ] ++ lib.optionals stdenv.isDarwin [ libiconv ]; + ] ++ lib.optionals stdenv.isDarwin ( + with darwin.apple_sdk.frameworks; [ CoreServices ]); }; }); } diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..ad0941a --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,54 @@ +use super::config; +use super::config::PathConfig; +use ansi_term::Colour::Green as Success; +use ansi_term::Colour::Yellow as Warning; +use std::io; +use std::io::Write; + +// TODO(jrpotter): Use curses to make this module behave nicer. + +pub fn write_config(mut pending: PathConfig) -> config::Result<()> { + println!( + "Generating config at {}...\n", + Success.paint(pending.0.display().to_string()) + ); + + print!( + "Git repository owner <{}> (enter to continue): ", + Warning.paint(pending.1.remote.owner.trim()) + ); + io::stdout().flush()?; + let mut owner = String::new(); + io::stdin().read_line(&mut owner)?; + let owner = owner.trim().to_owned(); + if !owner.is_empty() { + pending.1.remote.owner = owner; + } + + print!( + "Git repository name <{}> (enter to continue): ", + Warning.paint(pending.1.remote.name.trim()) + ); + io::stdout().flush()?; + let mut name = String::new(); + io::stdin().read_line(&mut name)?; + let name = name.trim().to_owned(); + if !name.is_empty() { + pending.1.remote.name = name; + } + + pending.write()?; + println!("\nFinished writing configuration file."); + Ok(()) +} + +pub fn list_packages(config: PathConfig) { + println!( + "Listing packages in {}...\n", + Success.paint(config.0.display().to_string()) + ); + // TODO(jrpotter): Alphabetize the output list. + for (k, _) in config.1.packages { + println!("• {}", k); + } +} diff --git a/src/config.rs b/src/config.rs index 1e19a7c..c985f1f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -9,7 +9,7 @@ use std::{env, error, fmt, fs, io}; // Error // ======================================== -type Result = std::result::Result; +pub type Result = std::result::Result; #[derive(Debug)] pub enum Error { @@ -67,21 +67,29 @@ impl Config { pub fn new(contents: &str) -> Result { 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(), - } +#[derive(Debug)] +pub struct PathConfig(pub PathBuf, pub Config); + +impl PathConfig { + pub fn new(path: &Path, config: Option) -> Self { + PathConfig( + path.to_path_buf(), + config.unwrap_or(Config { + remote: Remote { + owner: "example-user".to_owned(), + name: "home-config".to_owned(), + }, + packages: HashMap::new(), + }), + ) } - pub fn save(&self, path: &Path) -> Result<()> { + pub fn write(&self) -> Result<()> { // TODO(jrpotter): Create backup file before overwriting. - let mut file = fs::File::create(path)?; - let serialized = serde_yaml::to_string(&self)?; + let mut file = fs::File::create(&self.0)?; + let serialized = serde_yaml::to_string(&self.1)?; file.write_all(serialized.as_bytes())?; Ok(()) } @@ -120,50 +128,18 @@ pub fn default_paths() -> Vec { paths } -pub fn load(paths: &Vec) -> Result<(&Path, Config)> { +pub fn load(candidates: &Vec) -> Result { // 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 { + for path in candidates { match fs::read_to_string(path) { Err(err) if err.kind() == io::ErrorKind::NotFound => continue, Err(err) => return Err(Error::FileError(err)), - Ok(contents) => return Ok((&path, Config::new(&contents)?)), + Ok(contents) => { + let config = Config::new(&contents)?; + return Ok(PathConfig::new(&path, Some(config))); + } } } 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) -} diff --git a/src/daemon.rs b/src/daemon.rs new file mode 100644 index 0000000..274f1d6 --- /dev/null +++ b/src/daemon.rs @@ -0,0 +1,26 @@ +use notify::{RecommendedWatcher, RecursiveMode, Watcher}; +use std::path::Path; +use std::sync::mpsc::channel; +use std::time::Duration; + +fn watch(path: &Path) -> notify::Result<()> { + // Create a channel to receive the events. + let (tx, rx) = channel(); + + // Automatically select the best implementation for your platform. + // You can also access each implementation directly e.g. INotifyWatcher. + let mut watcher: RecommendedWatcher = Watcher::new(tx, Duration::from_secs(2))?; + + // Add a path to be watched. All files and directories at that path and + // below will be monitored for changes. + watcher.watch(path, RecursiveMode::NonRecursive)?; + + // This is a simple loop, but you may want to use more complex logic here, + // for example to handle I/O. + loop { + match rx.recv() { + Ok(event) => println!("{:?}", event), + Err(e) => println!("watch error: {:?}", e), + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 5450a9b..f5631ef 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,63 +1,50 @@ +pub mod cli; pub mod config; +pub mod daemon; -use ansi_term::Colour::Green; -use config::Config; +use config::PathConfig; use std::error::Error; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; -pub fn run_add(paths: Vec) -> Result<(), config::Error> { - debug_assert!(!paths.is_empty(), "`run_init` paths empty"); - if paths.is_empty() { - return Err(config::Error::MissingConfig); - } +pub fn run_add(_candidates: Vec) -> Result<(), config::Error> { // TODO(jrpotter): Show $EDITOR that allows writing specific package. Ok(()) } -pub fn run_init(paths: Vec) -> 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 - // same configuration options and override the values present in the current - // YAML file. - match config::load(&paths) { - Ok((path, config)) => config::init(path, config), - // TODO(jrpotter): Verify I have permission to write at specified path. - // Make directories if necessary. - Err(config::Error::MissingConfig) => config::init(&paths[0], Config::default()), - Err(e) => Err(e), - } +pub fn run_daemon(_candidates: Vec) -> Result<(), Box> { + Ok(()) } -pub fn run_list(paths: Vec) -> 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(()) +pub fn run_init(candidates: Vec) -> Result<(), config::Error> { + match config::load(&candidates) { + // 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. + Ok(pending) => cli::write_config(pending), + // Otherwise create a new config file at the given location. We always + // assume we want to write to the first file in our priority list. If + // not, the user should specify which config they want to write using + // the `-c` flag. + // TODO(jrpotter): Verify I have permission to write at specified path. + // Make directories if necessary. + Err(config::Error::MissingConfig) if !candidates.is_empty() => { + let pending = PathConfig::new(&candidates[0], None); + cli::write_config(pending) } Err(e) => Err(e), } } -pub fn run_pull(_: &clap::ArgMatches) -> Result<(), Box> { +pub fn run_list(candidates: Vec) -> Result<(), config::Error> { + let loaded = config::load(&candidates)?; + cli::list_packages(loaded); Ok(()) } -pub fn run_push(_: &clap::ArgMatches) -> Result<(), Box> { +pub fn run_pull() -> Result<(), Box> { + Ok(()) +} + +pub fn run_push() -> Result<(), Box> { Ok(()) } diff --git a/src/main.rs b/src/main.rs index 3dc82a1..c89719f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,20 @@ use clap::{App, AppSettings, Arg}; +use std::error::Error; use std::path::PathBuf; +fn dispatch(paths: Vec, matches: clap::ArgMatches) -> Result<(), Box> { + match matches.subcommand() { + Some(("add", _)) => homesync::run_add(paths)?, + Some(("daemon", _)) => homesync::run_daemon(paths)?, + Some(("init", _)) => homesync::run_init(paths)?, + Some(("list", _)) => homesync::run_list(paths)?, + Some(("pull", _)) => homesync::run_pull()?, + Some(("push", _)) => homesync::run_push()?, + _ => unreachable!(), + }; + Ok(()) +} + fn main() { let matches = App::new("homesync") .about("Cross desktop configuration sync tool.") @@ -16,39 +30,19 @@ fn main() { .takes_value(true), ) .subcommand(App::new("add").about("Add new configuration to local repository")) + .subcommand(App::new("daemon").about("Start up a new homesync daemon")) .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(); - let paths = match matches.value_of("config") { + let candidates = match matches.value_of("config") { Some(path) => vec![PathBuf::from(path)], None => homesync::config::default_paths(), }; - match matches.subcommand() { - Some(("add", _)) => { - if let Err(e) = homesync::run_add(paths) { - eprintln!("{}", e); - } - } - 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!(), + if let Err(e) = dispatch(candidates, matches) { + eprintln!("{}", e); } }