Various cleanup and daemon template.

pull/3/head
Joshua Potter 2021-12-30 08:37:53 -05:00
parent b4369739ac
commit 8db3c4b4ab
7 changed files with 159 additions and 120 deletions

View File

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

View File

@ -15,9 +15,11 @@
buildInputs = [ buildInputs = [
cargo cargo
rls rls
libiconv
rustc rustc
rustfmt rustfmt
] ++ lib.optionals stdenv.isDarwin [ libiconv ]; ] ++ lib.optionals stdenv.isDarwin (
with darwin.apple_sdk.frameworks; [ CoreServices ]);
}; };
}); });
} }

54
src/cli.rs Normal file
View File

@ -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);
}
}

View File

@ -9,7 +9,7 @@ use std::{env, error, fmt, fs, io};
// Error // Error
// ======================================== // ========================================
type Result<T> = std::result::Result<T, Error>; pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug)] #[derive(Debug)]
pub enum Error { pub enum Error {
@ -67,21 +67,29 @@ 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 { #[derive(Debug)]
Config { pub struct PathConfig(pub PathBuf, pub Config);
impl PathConfig {
pub fn new(path: &Path, config: Option<Config>) -> Self {
PathConfig(
path.to_path_buf(),
config.unwrap_or(Config {
remote: Remote { remote: Remote {
owner: "example-user".to_owned(), owner: "example-user".to_owned(),
name: "home-config".to_owned(), name: "home-config".to_owned(),
}, },
packages: HashMap::new(), packages: HashMap::new(),
} }),
)
} }
pub fn save(&self, path: &Path) -> Result<()> { pub fn write(&self) -> Result<()> {
// TODO(jrpotter): Create backup file before overwriting. // TODO(jrpotter): Create backup file before overwriting.
let mut file = fs::File::create(path)?; let mut file = fs::File::create(&self.0)?;
let serialized = serde_yaml::to_string(&self)?; let serialized = serde_yaml::to_string(&self.1)?;
file.write_all(serialized.as_bytes())?; file.write_all(serialized.as_bytes())?;
Ok(()) Ok(())
} }
@ -120,50 +128,18 @@ pub fn default_paths() -> Vec<PathBuf> {
paths paths
} }
pub fn load(paths: &Vec<PathBuf>) -> Result<(&Path, Config)> { pub fn load(candidates: &Vec<PathBuf>) -> Result<PathConfig> {
// 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 candidates {
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((&path, Config::new(&contents)?)), Ok(contents) => {
let config = Config::new(&contents)?;
return Ok(PathConfig::new(&path, Some(config)));
}
} }
} }
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)
}

26
src/daemon.rs Normal file
View File

@ -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),
}
}
}

View File

@ -1,63 +1,50 @@
pub mod cli;
pub mod config; pub mod config;
pub mod daemon;
use ansi_term::Colour::Green; use config::PathConfig;
use config::Config;
use std::error::Error; use std::error::Error;
use std::path::{Path, PathBuf}; use std::path::PathBuf;
pub fn run_add(paths: Vec<PathBuf>) -> Result<(), config::Error> { pub fn run_add(_candidates: Vec<PathBuf>) -> Result<(), config::Error> {
debug_assert!(!paths.is_empty(), "`run_init` paths empty");
if paths.is_empty() {
return Err(config::Error::MissingConfig);
}
// TODO(jrpotter): Show $EDITOR that allows writing specific package. // TODO(jrpotter): Show $EDITOR that allows writing specific package.
Ok(()) Ok(())
} }
pub fn run_init(paths: Vec<PathBuf>) -> Result<(), config::Error> { pub fn run_daemon(_candidates: Vec<PathBuf>) -> Result<(), Box<dyn Error>> {
// TODO(jrpotter): Use a nonempty implementation instead of this. Ok(())
debug_assert!(!paths.is_empty(), "`run_init` paths empty"); }
if paths.is_empty() {
return Err(config::Error::MissingConfig); pub fn run_init(candidates: Vec<PathBuf>) -> Result<(), config::Error> {
} match config::load(&candidates) {
// 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
// same configuration options and override the values present in the current // the same configuration options and override the values present in the
// YAML file. // current YAML file.
match config::load(&paths) { Ok(pending) => cli::write_config(pending),
Ok((path, config)) => config::init(path, config), // 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. // TODO(jrpotter): Verify I have permission to write at specified path.
// Make directories if necessary. // Make directories if necessary.
Err(config::Error::MissingConfig) => config::init(&paths[0], Config::default()), Err(config::Error::MissingConfig) if !candidates.is_empty() => {
Err(e) => Err(e), let pending = PathConfig::new(&candidates[0], None);
} cli::write_config(pending)
}
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), Err(e) => Err(e),
} }
} }
pub fn run_pull(_: &clap::ArgMatches) -> Result<(), Box<dyn Error>> { pub fn run_list(candidates: Vec<PathBuf>) -> Result<(), config::Error> {
let loaded = config::load(&candidates)?;
cli::list_packages(loaded);
Ok(()) Ok(())
} }
pub fn run_push(_: &clap::ArgMatches) -> Result<(), Box<dyn Error>> { pub fn run_pull() -> Result<(), Box<dyn Error>> {
Ok(())
}
pub fn run_push() -> Result<(), Box<dyn Error>> {
Ok(()) Ok(())
} }

View File

@ -1,6 +1,20 @@
use clap::{App, AppSettings, Arg}; use clap::{App, AppSettings, Arg};
use std::error::Error;
use std::path::PathBuf; use std::path::PathBuf;
fn dispatch(paths: Vec<PathBuf>, matches: clap::ArgMatches) -> Result<(), Box<dyn Error>> {
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() { fn main() {
let matches = App::new("homesync") let matches = App::new("homesync")
.about("Cross desktop configuration sync tool.") .about("Cross desktop configuration sync tool.")
@ -16,39 +30,19 @@ fn main() {
.takes_value(true), .takes_value(true),
) )
.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("daemon").about("Start up a new homesync daemon"))
.subcommand(App::new("init").about("Initialize the homesync 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("list").about("See which packages homesync manages"))
.subcommand(App::new("pull").about("Pull remote repository into local repository")) .subcommand(App::new("pull").about("Pull remote repository into local repository"))
.subcommand(App::new("push").about("Push local repository to remote repository")) .subcommand(App::new("push").about("Push local repository to remote repository"))
.get_matches(); .get_matches();
let paths = match matches.value_of("config") { let candidates = match matches.value_of("config") {
Some(path) => vec![PathBuf::from(path)], Some(path) => vec![PathBuf::from(path)],
None => homesync::config::default_paths(), None => homesync::config::default_paths(),
}; };
match matches.subcommand() { if let Err(e) = dispatch(candidates, matches) {
Some(("add", _)) => {
if let Err(e) = homesync::run_add(paths) {
eprintln!("{}", e); 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!(),
}
} }