From 2be25a330fd2d7d8626a9e4d8bff79dc5bb2c083 Mon Sep 17 00:00:00 2001 From: Dmitriy Pleshevskiy Date: Tue, 8 Nov 2022 00:07:21 +0300 Subject: [PATCH 1/5] domain: add optional on_line callback --- src/cli/switch.rs | 1 + src/domain.rs | 2 +- src/domain/switch.rs | 15 +++++++++++---- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/cli/switch.rs b/src/cli/switch.rs index 1f4c514..d346425 100644 --- a/src/cli/switch.rs +++ b/src/cli/switch.rs @@ -51,6 +51,7 @@ pub fn execute(args: &Args) -> Result<(), Error> { content: &content, writer, sections: &args.sections, + on_line: None, }) .map_err(Error::Switch) } diff --git a/src/domain.rs b/src/domain.rs index d35672a..88f0ab6 100644 --- a/src/domain.rs +++ b/src/domain.rs @@ -44,7 +44,7 @@ impl Section { } } -struct SectionInfo { +pub(crate) struct SectionInfo { enable_variable: bool, disable_variable: bool, } diff --git a/src/domain/switch.rs b/src/domain/switch.rs index ecf32bf..e499da4 100644 --- a/src/domain/switch.rs +++ b/src/domain/switch.rs @@ -33,6 +33,8 @@ impl std::fmt::Display for Error { impl std::error::Error for Error {} +pub type OnLineFn = Box, &String)>; + pub struct Request<'args, W> where W: Write, @@ -40,6 +42,7 @@ where pub content: &'args str, pub writer: W, pub sections: &'args [String], + pub on_line: Option, } pub fn execute(req: Request) -> Result<(), Error> @@ -54,14 +57,14 @@ where .map(|s| Section::parse(s.as_str())) .collect::>(); - let mut current_sections: Option = None; + let mut current_section: Option = None; for line in req.content.split_inclusive('\n') { let new_line = if is_section_end(line) { - current_sections = None; + current_section = None; line.to_string() } else if let Some(section_info) = line.strip_prefix("### ") { - current_sections = section_info.split_whitespace().next().map(|r| { + current_section = section_info.split_whitespace().next().map(|r| { let current_sections = r.split(',').map(Section::parse).collect::>(); SectionInfo { enable_variable: should_enable_variable(&choose_sections, ¤t_sections), @@ -69,7 +72,7 @@ where } }); line.to_string() - } else if let Some(section_info) = current_sections.as_ref() { + } else if let Some(section_info) = current_section.as_ref() { let trimmed_line = line.trim_start_matches(['#', ' ']); let is_var = is_variable(trimmed_line); if is_var && section_info.enable_variable { @@ -86,6 +89,10 @@ where writer .write_all(new_line.as_bytes()) .map_err(|_| Error::WriteData)?; + + if let Some(on_line) = req.on_line.as_ref() { + on_line(current_section.as_ref(), &new_line); + } } writer.flush().map_err(|_| Error::WriteData) -- 2.46.1 From 4e9ea6c68317afc1ab9262c55c6342ac5c8771cd Mon Sep 17 00:00:00 2001 From: Dmitriy Pleshevskiy Date: Thu, 10 Nov 2022 00:19:18 +0300 Subject: [PATCH 2/5] add colors --- .envrc | 1 - .gitignore | 6 +++- Cargo.lock | 1 + Cargo.toml | 3 +- flake.nix | 28 ++++++++++++++----- src/cli/switch.rs | 54 +++++++++++++++++++++++++----------- src/domain.rs | 2 +- src/domain/switch.rs | 65 ++++++++++---------------------------------- 8 files changed, 82 insertions(+), 78 deletions(-) delete mode 100644 .envrc 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] -- 2.46.1 From bbd1d47ba5798b994e723d05ccf410525b89a6e6 Mon Sep 17 00:00:00 2001 From: Dmitriy Pleshevskiy Date: Sat, 12 Nov 2022 16:25:35 +0300 Subject: [PATCH 3/5] cli: add color option --- Cargo.lock | 1 + Cargo.toml | 3 ++- src/cli.rs | 61 ++++++++++++++++++++++++++++++++++++++++++++++- src/cli/switch.rs | 28 ++++++++++++++-------- 4 files changed, 81 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f8a41c1..a0dec75 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -196,6 +196,7 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" name = "vnetod" version = "0.3.3" dependencies = [ + "atty", "clap", "termcolor", ] diff --git a/Cargo.toml b/Cargo.toml index 9ea72fe..046604e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,8 @@ categories = ["command-line-interface", "config", "development-tools"] [dependencies] clap = { version = "3.2.15", default-features = false, features = ["std", "env", "derive"] } +atty = { version = "0.2.14", optional = true } termcolor = { version = "1.1.3", optional = true } [features] -color = ["clap/color", "dep:termcolor"] +color = ["clap/color", "dep:atty", "dep:termcolor"] diff --git a/src/cli.rs b/src/cli.rs index e0d0b2a..ba2bf49 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -20,7 +20,7 @@ use std::path::PathBuf; use clap::Parser; -#[derive(Parser)] +#[derive(Parser, Debug)] #[clap( author, version, @@ -62,4 +62,63 @@ pub struct Args { help = "Environment varible sections that will be enabled" )] pub sections: Vec, + + #[cfg(feature = "color")] + #[clap( + long, + value_enum, + default_value = "auto", + long_help = " +This flag controls when to use colors. The default setting is 'auto', which +means vnetod will try to guess when to use colors. For example, if vnetod is +printing to a terminal, then it will use colors, but if it is redirected to a +file or a pipe, then it will suppress color output. vnetod will suppress color +output in some other circumstances as well. For example, if the TERM +environment variable is not set or set to 'dumb', then vnetod will not use +colors. + +The possible values for this flag are: + +never Colors will never be used. +auto The default. vnetod tries to be smart. +always Colors will always be used regardless of where output is sent. +ansi Like 'always', but emits ANSI escapes (even in a Windows console). + +" + )] + pub color: ColorVariant, +} + +#[cfg(feature = "color")] +#[derive(clap::ValueEnum, Clone, Debug)] +pub enum ColorVariant { + Auto, + Always, + Ansi, + Never, +} + +#[cfg(feature = "color")] +impl From for termcolor::ColorChoice { + fn from(col: ColorVariant) -> Self { + match col { + ColorVariant::Never => Self::Never, + ColorVariant::Always => Self::Always, + ColorVariant::Ansi => Self::AlwaysAnsi, + ColorVariant::Auto => { + if atty::is(atty::Stream::Stdout) { + // Otherwise let's `termcolor` decide by inspecting the environment. From the [doc]: + // - If `NO_COLOR` is set to any value, then colors will be suppressed. + // - If `TERM` is set to dumb, then colors will be suppressed. + // - In non-Windows environments, if `TERM` is not set, then colors will be suppressed. + // + // [doc]: https://github.com/BurntSushi/termcolor#automatic-color-selection + Self::Auto + } else { + // Colors should be deactivated if the terminal is not a tty. + Self::Never + } + } + } + } } diff --git a/src/cli/switch.rs b/src/cli/switch.rs index 27e8a13..b20f699 100644 --- a/src/cli/switch.rs +++ b/src/cli/switch.rs @@ -15,7 +15,7 @@ //! #[cfg(feature = "color")] -use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor}; +use termcolor::{Color, ColorSpec, StandardStream, WriteColor}; use crate::{cli::Args, domain}; use std::fs::File; @@ -47,11 +47,24 @@ pub fn execute(args: &Args) -> Result<(), Error> { }) .transpose()?; + #[cfg(feature = "color")] + let color = args.color.clone(); + + println!(); + let new_content = domain::switch::execute(domain::switch::Request { - content: &content, + content: &content.trim(), sections: &args.sections, - on_line: Some(Box::new(print_line)), + on_line: Some(Box::new(move |line| { + #[cfg(feature = "color")] + print_line(line, color.clone()); + #[cfg(not(feature = "color"))] + print!("{}", line) + })), }); + + println!(); + if let Some(mut fs_writer) = fs_writer { fs_writer .write_all(new_content.as_bytes()) @@ -62,8 +75,8 @@ pub fn execute(args: &Args) -> Result<(), Error> { } #[cfg(feature = "color")] -fn print_line(line: &String) { - let mut stdout = StandardStream::stdout(ColorChoice::Always); +fn print_line(line: &String, color: crate::cli::ColorVariant) { + let mut stdout = StandardStream::stdout(color.into()); let color = line .starts_with("###") .then_some(Color::Yellow) @@ -72,8 +85,3 @@ fn print_line(line: &String) { write!(&mut stdout, "{}", line).unwrap(); stdout.reset().ok(); } - -#[cfg(not(feature = "color"))] -fn print_line(line: &String) { - print!("{}", line) -} -- 2.46.1 From 8f81bddeaaee7be91a419f10df02eff366478ade Mon Sep 17 00:00:00 2001 From: Dmitriy Pleshevskiy Date: Sat, 12 Nov 2022 16:36:53 +0300 Subject: [PATCH 4/5] cli: add message for dry-run option --- src/cli/switch.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/cli/switch.rs b/src/cli/switch.rs index b20f699..dcfd6d6 100644 --- a/src/cli/switch.rs +++ b/src/cli/switch.rs @@ -41,6 +41,10 @@ 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)?; + if args.dry_run { + println!("Your file will be changed to the following") + } + let fs_writer = (!args.dry_run) .then(|| { File::create(args.output.as_ref().unwrap_or(&args.file)).map_err(|_| Error::OpenFile) -- 2.46.1 From 542d7f1d5c72e10638913a046cdaa91ddd9dd82e Mon Sep 17 00:00:00 2001 From: Dmitriy Pleshevskiy Date: Sat, 12 Nov 2022 16:37:16 +0300 Subject: [PATCH 5/5] nix: use colored version for docker --- flake.nix | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/flake.nix b/flake.nix index e5847b7..2a52409 100644 --- a/flake.nix +++ b/flake.nix @@ -48,20 +48,19 @@ config = { Volumes."/data" = { }; WorkingDir = "/data"; - Entrypoint = [ "${vnetod}/bin/vnetod" ]; + Entrypoint = [ "${coloredVnetod}/bin/vnetod" ]; }; }; + + mkApp = prog: { + type = "app"; + program = "${vnetod}/bin/vnetod"; + }; in { apps = { - default = { - type = "app"; - program = "${vnetod}/bin/vnetod"; - }; - colored = { - type = "app"; - program = "${coloredVnetod}/bin/vnetod"; - }; + default = mkApp vnetod; + colored = mkApp coloredVnetod; }; packages = { -- 2.46.1