diff --git a/.envrc b/.envrc deleted file mode 100644 index 3550a30..0000000 --- a/.envrc +++ /dev/null @@ -1 +0,0 @@ -use flake diff --git a/.gitignore b/.gitignore index c0dc59d..4592497 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,11 @@ +# build /target /result -.direnv +# environments .env* !.env.example +# direnv +.envrc +.direnv diff --git a/Cargo.lock b/Cargo.lock index 98951b4..f8a41c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -197,6 +197,7 @@ name = "vnetod" version = "0.3.3" dependencies = [ "clap", + "termcolor", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index fc6b11e..9ea72fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ categories = ["command-line-interface", "config", "development-tools"] [dependencies] clap = { version = "3.2.15", default-features = false, features = ["std", "env", "derive"] } +termcolor = { version = "1.1.3", optional = true } [features] -color = ["clap/color"] +color = ["clap/color", "dep:termcolor"] diff --git a/flake.nix b/flake.nix index 4ae4d21..e5847b7 100644 --- a/flake.nix +++ b/flake.nix @@ -11,22 +11,28 @@ cargoToml = fromTOML (readFile ./Cargo.toml); version = "${cargoToml.package.version}+${substring 0 8 self.lastModifiedDate}.${self.shortRev or "dirty"}"; - mkVnetod = { lib, rustPlatform, ... }: + mkVnetod = { lib, rustPlatform, vnetodFeatures ? [ ], ... }: rustPlatform.buildRustPackage { name = "vnetod-${version}"; src = lib.cleanSource ./.; cargoLock.lockFile = ./Cargo.lock; + buildFeatures = vnetodFeatures; + doCheck = true; }; in { - overlays = rec { - vnetod = final: prev: { + overlays = { + default = final: prev: { vnetod = final.callPackage mkVnetod { }; }; - default = vnetod; + colored = final: prev: { + vnetod = final.callPackage mkVnetod { + vnetodFeatures = [ "color" ]; + }; + }; }; } // flake-utils.lib.eachDefaultSystem (system: @@ -34,6 +40,7 @@ pkgs = import nixpkgs { inherit system; }; vnetod = pkgs.callPackage mkVnetod { }; + coloredVnetod = pkgs.callPackage mkVnetod { vnetodFeatures = [ "color" ]; }; docker = pkgs.dockerTools.buildLayeredImage { name = "pleshevskiy/vnetod"; @@ -46,14 +53,21 @@ }; in { - apps.default = { - type = "app"; - program = "${vnetod}/bin/vnetod"; + apps = { + default = { + type = "app"; + program = "${vnetod}/bin/vnetod"; + }; + colored = { + type = "app"; + program = "${coloredVnetod}/bin/vnetod"; + }; }; packages = { inherit docker vnetod; default = vnetod; + colored = coloredVnetod; }; devShell = pkgs.mkShell { diff --git a/src/cli/switch.rs b/src/cli/switch.rs index d346425..27e8a13 100644 --- a/src/cli/switch.rs +++ b/src/cli/switch.rs @@ -14,21 +14,24 @@ //! along with vnetod. If not, see . //! +#[cfg(feature = "color")] +use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; + use crate::{cli::Args, domain}; use std::fs::File; -use std::io::{stdout, Write}; +use std::io::Write; #[derive(Debug)] pub enum Error { OpenFile, - Switch(domain::switch::Error), + WriteFile, } impl std::fmt::Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Error::OpenFile => f.write_str("Cannot open file"), - Error::Switch(inner) => write!(f, "Cannot switch between states: {}", inner), + Error::WriteFile => f.write_str("Cannot write file"), } } } @@ -38,20 +41,39 @@ impl std::error::Error for Error {} pub fn execute(args: &Args) -> Result<(), Error> { let content = std::fs::read_to_string(&args.file).map_err(|_| Error::OpenFile)?; - let writer: Box = if args.dry_run { - Box::new(stdout()) - } else { - Box::new( - File::create(args.output.as_ref().unwrap_or(&args.file)) - .map_err(|_| Error::OpenFile)?, - ) - }; + let fs_writer = (!args.dry_run) + .then(|| { + File::create(args.output.as_ref().unwrap_or(&args.file)).map_err(|_| Error::OpenFile) + }) + .transpose()?; - domain::switch::execute(domain::switch::Request { + let new_content = domain::switch::execute(domain::switch::Request { content: &content, - writer, sections: &args.sections, - on_line: None, - }) - .map_err(Error::Switch) + on_line: Some(Box::new(print_line)), + }); + if let Some(mut fs_writer) = fs_writer { + fs_writer + .write_all(new_content.as_bytes()) + .map_err(|_| Error::WriteFile)?; + } + + Ok(()) +} + +#[cfg(feature = "color")] +fn print_line(line: &String) { + let mut stdout = StandardStream::stdout(ColorChoice::Always); + let color = line + .starts_with("###") + .then_some(Color::Yellow) + .or_else(|| (!line.starts_with("#")).then_some(Color::Green)); + stdout.set_color(ColorSpec::new().set_fg(color)).ok(); + write!(&mut stdout, "{}", line).unwrap(); + stdout.reset().ok(); +} + +#[cfg(not(feature = "color"))] +fn print_line(line: &String) { + print!("{}", line) } diff --git a/src/domain.rs b/src/domain.rs index 88f0ab6..960684f 100644 --- a/src/domain.rs +++ b/src/domain.rs @@ -44,7 +44,7 @@ impl Section { } } -pub(crate) struct SectionInfo { +pub struct SectionInfo { enable_variable: bool, disable_variable: bool, } diff --git a/src/domain/switch.rs b/src/domain/switch.rs index e499da4..01882e7 100644 --- a/src/domain/switch.rs +++ b/src/domain/switch.rs @@ -14,43 +14,17 @@ //! along with vnetod. If not, see . //! -use std::io::{BufWriter, Write}; - use super::{Section, SectionInfo}; -#[derive(Debug)] -pub enum Error { - WriteData, -} +pub type OnLineFn = Box; -impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Error::WriteData => f.write_str("Cannot write data"), - } - } -} - -impl std::error::Error for Error {} - -pub type OnLineFn = Box, &String)>; - -pub struct Request<'args, W> -where - W: Write, -{ +pub struct Request<'args> { pub content: &'args str, - pub writer: W, pub sections: &'args [String], pub on_line: Option, } -pub fn execute(req: Request) -> Result<(), Error> -where - W: Write, -{ - let mut writer = BufWriter::new(req.writer); - +pub fn execute(req: Request) -> String { let choose_sections = req .sections .iter() @@ -58,6 +32,7 @@ where .collect::>(); let mut current_section: Option = None; + let mut new_content = String::new(); for line in req.content.split_inclusive('\n') { let new_line = if is_section_end(line) { @@ -86,16 +61,13 @@ where line.to_string() }; - writer - .write_all(new_line.as_bytes()) - .map_err(|_| Error::WriteData)?; - + new_content.push_str(&new_line); if let Some(on_line) = req.on_line.as_ref() { - on_line(current_section.as_ref(), &new_line); + on_line(&new_line); } } - writer.flush().map_err(|_| Error::WriteData) + new_content } fn is_variable(trimmed_line: &str) -> bool { @@ -151,28 +123,19 @@ fn should_disable_variable(choose_sections: &[Section], current_sections: &[Sect #[cfg(test)] mod tests { use super::*; - use std::io::Cursor; const BASE_ENV: &str = include_str!("../../test_data/base_env"); fn make_test(input: &str, expected_output: &str, sections: Vec<&str>) { - let mut output_data = vec![]; - let writer = Cursor::new(&mut output_data); - - match execute(Request { + let output_data = execute(Request { content: input, - writer, sections: §ions.into_iter().map(String::from).collect::>(), - }) { - Ok(()) => { - let output = String::from_utf8(output_data).unwrap(); - assert_eq!( - output.lines().collect::>(), - expected_output.lines().collect::>() - ); - } - _ => unreachable!(), - } + on_line: None, + }); + assert_eq!( + output_data.lines().collect::>(), + expected_output.lines().collect::>() + ); } #[test]