fully flesh out CLI

This commit is contained in:
Akshay 2021-10-19 15:58:46 +05:30
parent 0076b3a37d
commit 214e3bc4b6
6 changed files with 633 additions and 73 deletions

306
Cargo.lock generated
View File

@ -3,10 +3,13 @@
version = 3
[[package]]
name = "anyhow"
version = "1.0.44"
name = "aho-corasick"
version = "0.7.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1"
checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f"
dependencies = [
"memchr",
]
[[package]]
name = "ariadne"
@ -17,12 +20,38 @@ dependencies = [
"yansi",
]
[[package]]
name = "atty"
version = "0.2.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
dependencies = [
"hermit-abi",
"libc",
"winapi",
]
[[package]]
name = "autocfg"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bstr"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223"
dependencies = [
"memchr",
]
[[package]]
name = "cbitset"
version = "0.2.0"
@ -32,24 +61,114 @@ dependencies = [
"num-traits",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
version = "3.0.0-beta.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcd70aa5597dbc42f7217a543f9ef2768b2ef823ba29036072d30e1d88e98406"
dependencies = [
"atty",
"bitflags",
"clap_derive",
"indexmap",
"lazy_static",
"os_str_bytes",
"strsim",
"termcolor",
"textwrap",
"vec_map",
]
[[package]]
name = "clap_derive"
version = "3.0.0-beta.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b5bb0d655624a0b8770d1c178fb8ffcb1f91cc722cb08f451e3dc72465421ac"
dependencies = [
"heck",
"proc-macro-error",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "countme"
version = "2.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "328b822bdcba4d4e402be8d9adb6eebf269f969f8eadef977a553ff3c4fbcb58"
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "globset"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10463d9ff00a2a068db14231982f5132edebad0d7660cd956a1c30292dbcbfbd"
dependencies = [
"aho-corasick",
"bstr",
"fnv",
"log",
"regex",
]
[[package]]
name = "hashbrown"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04"
[[package]]
name = "hashbrown"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
[[package]]
name = "heck"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c"
dependencies = [
"unicode-segmentation",
]
[[package]]
name = "hermit-abi"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
dependencies = [
"libc",
]
[[package]]
name = "if_chain"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed"
[[package]]
name = "indexmap"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc633605454125dec4b66843673f01c7df2b89479b32e0ed634e43a91cff62a5"
dependencies = [
"autocfg",
"hashbrown 0.11.2",
]
[[package]]
name = "lazy_static"
version = "1.4.0"
@ -67,6 +186,21 @@ dependencies = [
"rowan",
]
[[package]]
name = "libc"
version = "0.2.103"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8f7255a17a627354f321ef0055d63b898c6fb27eff628af4d1b66b7331edf6"
[[package]]
name = "log"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
dependencies = [
"cfg-if",
]
[[package]]
name = "macros"
version = "0.1.0"
@ -76,6 +210,12 @@ dependencies = [
"syn",
]
[[package]]
name = "memchr"
version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
[[package]]
name = "memoffset"
version = "0.6.4"
@ -94,6 +234,36 @@ dependencies = [
"autocfg",
]
[[package]]
name = "os_str_bytes"
version = "3.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6acbef58a60fe69ab50510a55bc8cdd4d6cf2283d27ad338f54cb52747a9cf2d"
[[package]]
name = "proc-macro-error"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
"proc-macro-error-attr",
"proc-macro2",
"quote",
"syn",
"version_check",
]
[[package]]
name = "proc-macro-error-attr"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
"proc-macro2",
"quote",
"version_check",
]
[[package]]
name = "proc-macro2"
version = "1.0.29"
@ -112,6 +282,23 @@ dependencies = [
"proc-macro2",
]
[[package]]
name = "regex"
version = "1.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.6.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
[[package]]
name = "rnix"
version = "0.9.0"
@ -130,7 +317,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1b36e449f3702f3b0c821411db1cbdf30fb451726a9456dce5dabcd44420043"
dependencies = [
"countme",
"hashbrown",
"hashbrown 0.9.1",
"memoffset",
"rustc-hash",
"text-size",
@ -152,12 +339,21 @@ checksum = "b203e79e90905594272c1c97c7af701533d42adaab0beb3859018e477d54a3b0"
name = "statix"
version = "0.1.0"
dependencies = [
"anyhow",
"ariadne",
"clap",
"globset",
"lib",
"rnix",
"thiserror",
"vfs",
]
[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "syn"
version = "1.0.76"
@ -169,18 +365,118 @@ dependencies = [
"unicode-xid",
]
[[package]]
name = "termcolor"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dfed899f0eb03f32ee8c6a0aabdb8a7949659e3466561fc0adf54e26d88c5f4"
dependencies = [
"winapi-util",
]
[[package]]
name = "text-size"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "288cb548dbe72b652243ea797201f3d481a0609a967980fcc5b2315ea811560a"
[[package]]
name = "textwrap"
version = "0.14.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80"
dependencies = [
"unicode-width",
]
[[package]]
name = "thiserror"
version = "1.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-segmentation"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
[[package]]
name = "unicode-width"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
[[package]]
name = "unicode-xid"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3"
[[package]]
name = "vec_map"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
[[package]]
name = "version_check"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
[[package]]
name = "vfs"
version = "0.1.0"
dependencies = [
"indexmap",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
dependencies = [
"winapi",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "yansi"
version = "0.5.0"

View File

@ -8,5 +8,8 @@ edition = "2018"
[dependencies]
lib = { path = "../lib" }
ariadne = "0.1.3"
anyhow = "1.0"
rnix = "0.9.0"
clap = "3.0.0-beta.4"
globset = "0.4.8"
thiserror = "1.0.30"
vfs = { path = "../vfs" }

170
bin/src/config.rs Normal file
View File

@ -0,0 +1,170 @@
use std::{default::Default, fs, path::PathBuf, str::FromStr};
use clap::Clap;
use globset::{GlobBuilder, GlobSetBuilder};
use vfs::ReadOnlyVfs;
use crate::err::ConfigErr;
/// Static analysis and linting for the nix programming language
#[derive(Clap, Debug)]
#[clap(version = "0.1.0", author = "Akshay <nerdy@peppe.rs>")]
pub struct Opts {
/// File or directory to run statix on
#[clap(default_value = ".")]
target: String,
// /// Path to statix config
// #[clap(short, long, default_value = ".statix.toml")]
// config: String,
/// Regex of file patterns to not lint
#[clap(short, long)]
ignore: Vec<String>,
/// Output format. Supported values: json, errfmt
#[clap(short = 'o', long)]
format: Option<OutFormat>,
#[clap(subcommand)]
pub subcmd: Option<SubCommand>,
}
#[derive(Clap, Debug)]
#[clap(version = "0.1.0", author = "Akshay <nerdy@peppe.rs>")]
pub enum SubCommand {
/// Find and fix issues raised by statix
Fix(Fix),
}
#[derive(Clap, Debug)]
pub struct Fix {
/// Do not write to files, display a diff instead
#[clap(short = 'd', long = "dry-run")]
diff_only: bool,
}
#[derive(Debug, Copy, Clone)]
pub enum OutFormat {
Json,
Errfmt,
StdErr,
}
impl Default for OutFormat {
fn default() -> Self {
OutFormat::StdErr
}
}
impl FromStr for OutFormat {
type Err = &'static str;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value.to_ascii_lowercase().as_str() {
"json" => Ok(Self::Json),
"errfmt" => Ok(Self::Errfmt),
"stderr" => Ok(Self::StdErr),
_ => Err("unknown output format, try: json, errfmt"),
}
}
}
#[derive(Debug)]
pub struct LintConfig {
pub files: Vec<PathBuf>,
pub format: OutFormat,
}
impl LintConfig {
pub fn from_opts(opts: Opts) -> Result<Self, ConfigErr> {
let ignores = {
let mut set = GlobSetBuilder::new();
for pattern in opts.ignore {
let glob = GlobBuilder::new(&pattern).build().map_err(|err| {
ConfigErr::InvalidGlob(err.glob().map(|i| i.to_owned()), err.kind().clone())
})?;
set.add(glob);
}
set.build().map_err(|err| {
ConfigErr::InvalidGlob(err.glob().map(|i| i.to_owned()), err.kind().clone())
})
}?;
let walker = dirs::Walker::new(opts.target).map_err(ConfigErr::InvalidPath)?;
let files = walker
.filter(|path| matches!(path.extension(), Some(e) if e == "nix"))
.filter(|path| !ignores.is_match(path))
.collect();
Ok(Self {
files,
format: opts.format.unwrap_or_default(),
})
}
pub fn vfs(&self) -> Result<ReadOnlyVfs, ConfigErr> {
let mut vfs = ReadOnlyVfs::default();
for file in self.files.iter() {
let _id = vfs.alloc_file_id(&file);
let data = fs::read_to_string(&file).map_err(ConfigErr::InvalidPath)?;
vfs.set_file_contents(&file, data.as_bytes());
}
Ok(vfs)
}
}
mod dirs {
use std::{
fs,
io::{self, Error, ErrorKind},
path::{Path, PathBuf},
};
#[derive(Default, Debug)]
pub struct Walker {
dirs: Vec<PathBuf>,
files: Vec<PathBuf>,
}
impl Walker {
pub fn new<P: AsRef<Path>>(target: P) -> io::Result<Self> {
let target = target.as_ref().to_path_buf();
if !target.exists() {
Err(Error::new(
ErrorKind::NotFound,
format!("file not found: {}", target.display()),
))
} else if target.is_dir() {
Ok(Self {
dirs: vec![target],
..Default::default()
})
} else {
Ok(Self {
files: vec![target],
..Default::default()
})
}
}
}
impl Iterator for Walker {
type Item = PathBuf;
fn next(&mut self) -> Option<Self::Item> {
if let Some(dir) = self.dirs.pop() {
if dir.is_dir() {
for entry in fs::read_dir(dir).ok()? {
let entry = entry.ok()?;
let path = entry.path();
if path.is_dir() {
self.dirs.push(path);
} else if path.is_file() {
self.files.push(path);
}
}
}
}
self.files.pop()
}
}
}

28
bin/src/err.rs Normal file
View File

@ -0,0 +1,28 @@
use std::{io, path::PathBuf};
use globset::ErrorKind;
use rnix::parser::ParseError;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ConfigErr {
#[error("error parsing glob `{0:?}`: {1}")]
InvalidGlob(Option<String>, ErrorKind),
#[error("path error: {0}")]
InvalidPath(#[from] io::Error),
}
#[derive(Error, Debug)]
pub enum LintErr {
#[error("[{0}] syntax error: {1}")]
Parse(PathBuf, ParseError),
}
#[derive(Error, Debug)]
pub enum StatixErr {
#[error("linter error: {0}")]
Lint(#[from] LintErr),
#[error("config error: {0}")]
Config(#[from] ConfigErr),
}

View File

@ -1,20 +1,28 @@
use std::{
env, fs,
path::{Path, PathBuf},
#![feature(path_try_exists)]
mod config;
mod err;
mod traits;
use std::io;
use crate::{
err::{LintErr, StatixErr},
traits::{LintResult, WriteDiagnostic},
};
use anyhow::{Context, Result};
use ariadne::{
CharSet, Color, Config as CliConfig, Label, LabelAttach, Report as CliReport,
ReportKind as CliReportKind, Source,
};
use lib::{Report, LINTS};
use rnix::{TextRange, WalkEvent};
use clap::Clap;
use config::{LintConfig, Opts, SubCommand};
use lib::LINTS;
use rnix::WalkEvent;
use vfs::VfsEntry;
fn analyze(source: &str) -> Result<Vec<Report>> {
let parsed = rnix::parse(source).as_result()?;
Ok(parsed
fn analyze<'ρ>(vfs_entry: VfsEntry<'ρ>) -> Result<LintResult, LintErr> {
let source = vfs_entry.contents;
let parsed = rnix::parse(source)
.as_result()
.map_err(|e| LintErr::Parse(vfs_entry.file_path.to_path_buf(), e))?;
let reports = parsed
.node()
.preorder_with_tokens()
.filter_map(|event| match event {
@ -27,61 +35,33 @@ fn analyze(source: &str) -> Result<Vec<Report>> {
_ => None,
})
.flatten()
.collect())
.collect();
Ok(LintResult {
file_id: vfs_entry.file_id,
reports,
})
}
fn print_report(report: Report, file_src: &str, file_path: &Path) -> Result<()> {
let range = |at: TextRange| at.start().into()..at.end().into();
let src_id = file_path.to_str().unwrap_or("<unknown>");
let offset = report
.diagnostics
.iter()
.map(|d| d.at.start().into())
.min()
.unwrap_or(0usize);
report
.diagnostics
.iter()
.fold(
CliReport::build(CliReportKind::Warning, src_id, offset)
.with_config(
CliConfig::default()
.with_cross_gap(true)
.with_multiline_arrows(false)
.with_label_attach(LabelAttach::Middle)
.with_char_set(CharSet::Unicode),
)
.with_message(report.note)
.with_code(report.code),
|cli_report, diagnostic| {
cli_report.with_label(
Label::new((src_id, range(diagnostic.at)))
.with_message(&diagnostic.message)
.with_color(Color::Magenta),
)
},
)
.finish()
.eprint((src_id, Source::from(file_src)))
.context("failed to print report to stdout")
}
fn _main() -> Result<()> {
fn _main() -> Result<(), StatixErr> {
// TODO: accept cli args, construct a CLI config with a list of files to analyze
let args = env::args();
for (file_src, file_path, reports) in args
.skip(1)
.map(|s| PathBuf::from(&s))
.filter(|p| p.is_file())
.filter_map(|path| {
let s = fs::read_to_string(&path).ok()?;
analyze(&s)
.map(|analysis_result| (s, path, analysis_result))
.ok()
})
{
for r in reports {
print_report(r, &file_src, &file_path)?
let opts = Opts::parse();
match opts.subcmd {
Some(SubCommand::Fix(_)) => {}
None => {
let lint_config = LintConfig::from_opts(opts)?;
let vfs = lint_config.vfs()?;
let (reports, errors): (Vec<_>, Vec<_>) =
vfs.iter().map(analyze).partition(Result::is_ok);
let lint_results: Vec<_> = reports.into_iter().map(Result::unwrap).collect();
let errors: Vec<_> = errors.into_iter().map(Result::unwrap_err).collect();
let mut stderr = io::stderr();
lint_results.into_iter().for_each(|r| {
stderr.write(&r, &vfs).unwrap();
});
errors.into_iter().for_each(|e| {
eprintln!("{}", e);
});
}
}
Ok(())
@ -90,6 +70,6 @@ fn _main() -> Result<()> {
fn main() {
match _main() {
Err(e) => eprintln!("{}", e),
_ => {}
_ => (),
}
}

83
bin/src/traits.rs Normal file
View File

@ -0,0 +1,83 @@
use std::{
io::{self, Write},
str,
};
use ariadne::{
CharSet, Color, Config as CliConfig, Label, LabelAttach, Report as CliReport,
ReportKind as CliReportKind, Source, Fmt
};
use lib::Report;
use rnix::TextRange;
use vfs::{FileId, ReadOnlyVfs};
#[derive(Debug)]
pub struct LintResult {
pub file_id: FileId,
pub reports: Vec<Report>,
}
pub trait WriteDiagnostic {
fn write(&mut self, report: &LintResult, vfs: &ReadOnlyVfs) -> io::Result<()>;
}
impl<T> WriteDiagnostic for T
where
T: Write,
{
fn write(&mut self, lint_result: &LintResult, vfs: &ReadOnlyVfs) -> io::Result<()> {
let file_id = lint_result.file_id;
let src = str::from_utf8(vfs.get(file_id)).unwrap();
let path = vfs.file_path(file_id);
let range = |at: TextRange| at.start().into()..at.end().into();
let src_id = path.to_str().unwrap_or("<unknown>");
for report in lint_result.reports.iter() {
let offset = report
.diagnostics
.iter()
.map(|d| d.at.start().into())
.min()
.unwrap_or(0usize);
report
.diagnostics
.iter()
.fold(
CliReport::build(CliReportKind::Warning, src_id, offset)
.with_config(
CliConfig::default()
.with_cross_gap(true)
.with_multiline_arrows(false)
.with_label_attach(LabelAttach::Middle)
.with_char_set(CharSet::Unicode),
)
.with_message(report.note)
.with_code(report.code),
|cli_report, diagnostic| {
cli_report.with_label(
Label::new((src_id, range(diagnostic.at)))
.with_message(&colorize(&diagnostic.message))
.with_color(Color::Magenta),
)
},
)
.finish()
.write((src_id, Source::from(src)), &mut *self)?;
}
Ok(())
}
}
// everything within backticks is colorized, backticks are removed
fn colorize(message: &str) -> String {
message.split('`')
.enumerate()
.map(|(idx, part)| {
if idx % 2 == 1 {
part.fg(Color::Cyan).to_string()
} else {
part.to_string()
}
})
.collect::<Vec<_>>()
.join("")
}