#[cfg(feature = "conf")]
use config;
#[cfg(feature = "conf")]
use directories::BaseDirs;
use crate::style::Style;
#[cfg(feature = "conf")]
use std::env;
use std::fmt::{self, Display};
use std::io::IsTerminal;
use std::marker::PhantomData;
use std::str::FromStr;
use serde::{
de::{self, Deserializer, MapAccess, Unexpected, Visitor},
Deserialize, Serialize,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum StyleWhen {
Never,
Always,
Tty,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(default)]
pub struct PrintConfig {
pub depth: u32,
pub indent: usize,
pub padding: usize,
pub styled: StyleWhen,
#[serde(deserialize_with = "string_or_struct")]
pub characters: IndentChars,
pub branch: Style,
pub leaf: Style,
}
impl Default for PrintConfig {
fn default() -> PrintConfig {
PrintConfig {
depth: u32::MAX,
indent: 3,
padding: 1,
characters: UTF_CHARS.into(),
branch: Style {
dimmed: true,
..Style::default()
},
leaf: Style::default(),
styled: StyleWhen::Tty,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutputKind {
Stdout,
Unknown,
}
impl PrintConfig {
#[cfg(feature = "conf")]
fn try_from_env() -> Option<PrintConfig> {
let mut builder = config::ConfigBuilder::<config::builder::DefaultState>::default();
if let Ok(p) = env::var("PTREE_CONFIG") {
builder = builder.add_source(config::File::with_name(&p));
} else {
let f = BaseDirs::new()?.config_dir().join("ptree");
builder = builder.add_source(config::File::with_name(f.to_str()?));
}
builder = builder.add_source(config::Environment::with_prefix("PTREE").separator("_"));
builder.build().ok()?.try_deserialize().ok()
}
#[cfg(feature = "conf")]
pub fn from_env() -> PrintConfig {
Self::try_from_env().unwrap_or_default()
}
#[cfg(not(feature = "conf"))]
pub fn from_env() -> PrintConfig {
Default::default()
}
pub fn should_style_output(&self, output_kind: OutputKind) -> bool {
if cfg!(feature = "ansi") {
match (self.styled, output_kind) {
(StyleWhen::Always, _) => true,
#[cfg(feature = "ansi")]
(StyleWhen::Tty, OutputKind::Stdout) => std::io::stdout().is_terminal(),
_ => false,
}
} else {
false
}
}
pub fn paint_branch(&self, input: impl Display) -> impl Display {
self.branch.paint(input)
}
pub fn paint_leaf(&self, input: impl Display) -> impl Display {
self.leaf.paint(input)
}
}
fn get_default_empty_string() -> String {
" ".to_string()
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct IndentChars {
pub down_and_right: String,
pub down: String,
pub turn_right: String,
pub right: String,
#[serde(default = "get_default_empty_string")]
pub empty: String,
}
impl From<StaticIndentChars> for IndentChars {
fn from(s: StaticIndentChars) -> IndentChars {
IndentChars {
down_and_right: s.down_and_right.to_string(),
down: s.down.to_string(),
turn_right: s.turn_right.to_string(),
right: s.right.to_string(),
empty: s.empty.to_string(),
}
}
}
impl FromStr for IndentChars {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"utf" => Ok(UTF_CHARS.into()),
"ascii" | "ascii-tick" => Ok(ASCII_CHARS_TICK.into()),
"ascii-plus" => Ok(ASCII_CHARS_PLUS.into()),
"utf-bold" => Ok(UTF_CHARS_BOLD.into()),
"utf-dashed" => Ok(UTF_CHARS_DASHED.into()),
"utf-double" => Ok(UTF_CHARS_DOUBLE.into()),
_ => Err(()),
}
}
}
fn string_or_struct<'de, T, D>(deserializer: D) -> Result<T, D::Error>
where
T: Deserialize<'de> + FromStr<Err = ()>,
D: Deserializer<'de>,
{
struct StringOrStruct<T>(PhantomData<fn() -> T>);
impl<'de, T> Visitor<'de> for StringOrStruct<T>
where
T: Deserialize<'de> + FromStr<Err = ()>,
{
type Value = T;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("string or map")
}
fn visit_str<E>(self, value: &str) -> Result<T, E>
where
E: de::Error,
{
FromStr::from_str(value).map_err(|_| {
E::invalid_value(
Unexpected::Str(value),
&"'utf', 'ascii', 'ascii-plus', 'utf-double', 'utf-bold' or 'utf-dashed'",
)
})
}
fn visit_map<M>(self, visitor: M) -> Result<T, M::Error>
where
M: MapAccess<'de>,
{
Deserialize::deserialize(de::value::MapAccessDeserializer::new(visitor))
}
}
deserializer.deserialize_any(StringOrStruct(PhantomData))
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct StaticIndentChars {
pub down_and_right: &'static str,
pub down: &'static str,
pub turn_right: &'static str,
pub right: &'static str,
pub empty: &'static str,
}
pub const ASCII_CHARS_TICK: StaticIndentChars = StaticIndentChars {
down_and_right: "|",
down: "|",
turn_right: "`",
right: "-",
empty: " ",
};
pub const ASCII_CHARS_PLUS: StaticIndentChars = StaticIndentChars {
down_and_right: "+",
down: "|",
turn_right: "+",
right: "-",
empty: " ",
};
pub const UTF_CHARS: StaticIndentChars = StaticIndentChars {
down_and_right: "├",
down: "│",
turn_right: "└",
right: "─",
empty: " ",
};
pub const UTF_CHARS_DOUBLE: StaticIndentChars = StaticIndentChars {
down_and_right: "╠",
down: "║",
turn_right: "╚",
right: "═",
empty: " ",
};
pub const UTF_CHARS_BOLD: StaticIndentChars = StaticIndentChars {
down_and_right: "┣",
down: "┃",
turn_right: "┗",
right: "━",
empty: " ",
};
pub const UTF_CHARS_DASHED: StaticIndentChars = StaticIndentChars {
down_and_right: "├",
down: "┆",
turn_right: "└",
right: "╌",
empty: " ",
};
#[cfg(test)]
mod tests {
use super::*;
use crate::style::Color;
use std::env;
use std::fs::{self, File};
use std::io::Write;
use std::sync::Mutex;
lazy_static! {
static ref ENV_MUTEX: Mutex<()> = Mutex::new(());
}
#[cfg(feature = "conf")]
fn load_config_from_path(path: &str) -> PrintConfig {
env::set_var("PTREE_CONFIG", path);
let config = PrintConfig::from_env();
env::remove_var("PTREE_CONFIG");
config
}
#[test]
#[cfg(feature = "conf")]
fn load_yaml_config_file() {
let _g = ENV_MUTEX.lock().unwrap();
let path = "ptree.yaml";
{
let mut f = File::create(path).unwrap();
writeln!(f, "indent: 7\nbranch:\n foreground: maroon").unwrap();
}
let config = load_config_from_path(path);
assert_eq!(config.indent, 7);
assert_eq!(config.branch.foreground, Some(Color::Named("maroon".to_string())));
assert_eq!(config.branch.background, None);
fs::remove_file(path).unwrap();
}
#[test]
#[cfg(feature = "conf")]
fn load_toml_config_file() {
let _g = ENV_MUTEX.lock().unwrap();
let path = "ptree.toml";
{
let mut f = File::create(path).unwrap();
writeln!(
f,
"indent = 5\n[leaf]\nforeground = \"green\"\nbackground = \"steelblue\"\n"
)
.unwrap();
}
let config = load_config_from_path(path);
assert_eq!(config.indent, 5);
assert_eq!(config.leaf.foreground, Some(Color::Named("green".to_string())));
assert_eq!(config.leaf.background, Some(Color::Named("steelblue".to_string())));
assert_eq!(config.branch.foreground, None);
assert_eq!(config.branch.background, None);
fs::remove_file(path).unwrap();
}
#[test]
#[cfg(feature = "conf")]
fn load_env() {
let _g = ENV_MUTEX.lock().unwrap();
let path = "ptree.toml";
{
let mut f = File::create(path).unwrap();
writeln!(f, "indent = 5\n[leaf]\nforeground = \"green\"\n").unwrap();
}
env::set_var("PTREE_LEAF_BACKGROUND", "steelblue");
env::set_var("PTREE_LEAF_BOLD", "true");
env::set_var("PTREE_DEPTH", "4");
let config = load_config_from_path(path);
assert_eq!(config.indent, 5);
assert_eq!(config.depth, 4);
assert_eq!(config.leaf.foreground, Some(Color::Named("green".to_string())));
assert_eq!(config.leaf.background, Some(Color::Named("steelblue".to_string())));
assert!(config.leaf.bold);
assert_eq!(config.branch.foreground, None);
assert_eq!(config.branch.background, None);
env::remove_var("PTREE_LEAF_BACKGROUND");
env::remove_var("PTREE_LEAF_BOLD");
env::remove_var("PTREE_DEPTH");
fs::remove_file(path).unwrap();
}
}