use colored::{Color as Col, ColoredString, Colorize};
use core::fmt::Formatter;
use serde::{
de::{self, Visitor},
Deserialize, Serialize,
};
use std::{
fmt, fs,
ops::Deref,
path::{Path, PathBuf},
str::FromStr,
};
use crate::error::Error;
#[derive(Debug, Deserialize, Serialize, Default)]
#[serde(default)]
pub struct Config {
pub main: MainConfig,
pub colors: ColorConfig,
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(default)]
pub struct MainConfig {
interactive_editor: Option<String>,
tasks_file: PathBuf,
todo_alias: String,
wip_alias: String,
done_alias: String,
cancelled_alias: String,
uid_col_name: String,
age_col_name: String,
spent_col_name: String,
prio_col_name: String,
project_col_name: String,
tags_col_name: String,
status_col_name: String,
description_col_name: String,
display_empty_cols: bool,
max_description_lines: usize,
notes_nb_col_name: String,
display_tags_listings: bool,
previous_notes_help: bool,
}
impl Default for MainConfig {
fn default() -> Self {
Self {
interactive_editor: None,
tasks_file: dirs::config_dir().unwrap().join("toodoux"),
todo_alias: "TODO".to_owned(),
wip_alias: "WIP".to_owned(),
done_alias: "DONE".to_owned(),
cancelled_alias: "CANCELLED".to_owned(),
uid_col_name: "UID".to_owned(),
age_col_name: "Age".to_owned(),
spent_col_name: "Spent".to_owned(),
prio_col_name: "Prio".to_owned(),
project_col_name: "Project".to_owned(),
tags_col_name: "Tags".to_owned(),
status_col_name: "Status".to_owned(),
description_col_name: "Description".to_owned(),
notes_nb_col_name: "Notes".to_owned(),
display_empty_cols: false,
max_description_lines: 2,
display_tags_listings: true,
previous_notes_help: true,
}
}
}
impl MainConfig {
#[allow(dead_code)]
pub fn new(
interactive_editor: impl Into<Option<String>>,
tasks_file: impl Into<PathBuf>,
todo_alias: impl Into<String>,
wip_alias: impl Into<String>,
done_alias: impl Into<String>,
cancelled_alias: impl Into<String>,
uid_col_name: impl Into<String>,
age_col_name: impl Into<String>,
spent_col_name: impl Into<String>,
prio_col_name: impl Into<String>,
project_col_name: impl Into<String>,
tags_col_name: impl Into<String>,
status_col_name: impl Into<String>,
description_col_name: impl Into<String>,
notes_nb_col_name: impl Into<String>,
display_empty_cols: bool,
max_description_lines: usize,
display_tags_listings: bool,
previous_notes_help: bool,
) -> Self {
Self {
interactive_editor: interactive_editor.into(),
tasks_file: tasks_file.into(),
todo_alias: todo_alias.into(),
wip_alias: wip_alias.into(),
done_alias: done_alias.into(),
cancelled_alias: cancelled_alias.into(),
uid_col_name: uid_col_name.into(),
age_col_name: age_col_name.into(),
spent_col_name: spent_col_name.into(),
prio_col_name: prio_col_name.into(),
project_col_name: project_col_name.into(),
tags_col_name: tags_col_name.into(),
status_col_name: status_col_name.into(),
description_col_name: description_col_name.into(),
notes_nb_col_name: notes_nb_col_name.into(),
display_empty_cols,
max_description_lines,
display_tags_listings,
previous_notes_help,
}
}
}
impl Config {
#[allow(dead_code)]
pub fn new(main: MainConfig, colors: ColorConfig) -> Self {
Config { main, colors }
}
fn get_config_path() -> Result<PathBuf, Error> {
log::trace!("getting configuration root path from the environment");
let home = dirs::config_dir().ok_or(Error::NoConfigDir)?;
let path = Path::new(&home).join("toodoux");
Ok(path)
}
pub fn from_dir(path: impl AsRef<Path>) -> Result<Option<Self>, Error> {
let path = path.as_ref().join("config.toml");
log::trace!("reading configuration from {}", path.display());
if path.is_file() {
let content = fs::read_to_string(&path).map_err(Error::CannotOpenFile)?;
let parsed = toml::from_str(&content).map_err(Error::CannotDeserializeFromTOML)?;
Ok(Some(parsed))
} else {
Ok(None)
}
}
pub fn root_dir(&self) -> &Path {
&self.main.tasks_file
}
pub fn config_toml_path(&self) -> PathBuf {
self.main.tasks_file.join("config.toml")
}
pub fn interactive_editor(&self) -> Option<&str> {
self.main.interactive_editor.as_deref()
}
pub fn tasks_path(&self) -> PathBuf {
self.main.tasks_file.join("tasks.json")
}
pub fn todo_alias(&self) -> &str {
&self.main.todo_alias
}
pub fn wip_alias(&self) -> &str {
&self.main.wip_alias
}
pub fn done_alias(&self) -> &str {
&self.main.done_alias
}
pub fn cancelled_alias(&self) -> &str {
&self.main.cancelled_alias
}
pub fn uid_col_name(&self) -> &str {
&self.main.uid_col_name
}
pub fn age_col_name(&self) -> &str {
&self.main.age_col_name
}
pub fn spent_col_name(&self) -> &str {
&self.main.spent_col_name
}
pub fn prio_col_name(&self) -> &str {
&self.main.prio_col_name
}
pub fn project_col_name(&self) -> &str {
&self.main.project_col_name
}
pub fn tags_col_name(&self) -> &str {
&self.main.tags_col_name
}
pub fn status_col_name(&self) -> &str {
&self.main.status_col_name
}
pub fn description_col_name(&self) -> &str {
&self.main.description_col_name
}
pub fn notes_nb_col_name(&self) -> &str {
&self.main.notes_nb_col_name
}
pub fn display_empty_cols(&self) -> bool {
self.main.display_empty_cols
}
pub fn max_description_lines(&self) -> usize {
self.main.max_description_lines
}
pub fn display_tags_listings(&self) -> bool {
self.main.display_tags_listings
}
pub fn previous_notes_help(&self) -> bool {
self.main.previous_notes_help
}
pub fn get() -> Result<Option<Self>, Error> {
let path = Self::get_config_path()?;
Self::from_dir(path)
}
pub fn create(path: Option<&Path>) -> Result<Self, Error> {
let default_config = Self::default();
let tasks_file = path.map_or_else(Self::get_config_path, |p| Ok(p.to_owned()))?;
let main = MainConfig {
tasks_file,
..default_config.main
};
let config = Self {
main,
..default_config
};
log::trace!("creating new configuration:\n{:#?}", config);
Ok(config)
}
pub fn save(&self) -> Result<(), Error> {
let root_dir = self.root_dir();
fs::create_dir_all(root_dir).map_err(Error::CannotSave)?;
let serialized = toml::to_string_pretty(self).map_err(Error::CannotSerializeToTOML)?;
fs::write(self.config_toml_path(), serialized).map_err(Error::CannotSave)?;
Ok(())
}
}
#[derive(Debug, Deserialize, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum StyleAttribute {
Bold,
Dimmed,
Underline,
Reversed,
Italic,
Blink,
Hidden,
Strikethrough,
}
impl StyleAttribute {
fn apply_style(&self, s: ColoredString) -> ColoredString {
match self {
StyleAttribute::Bold => s.bold(),
StyleAttribute::Dimmed => s.dimmed(),
StyleAttribute::Underline => s.underline(),
StyleAttribute::Reversed => s.reversed(),
StyleAttribute::Italic => s.italic(),
StyleAttribute::Blink => s.blink(),
StyleAttribute::Hidden => s.hidden(),
StyleAttribute::Strikethrough => s.strikethrough(),
}
}
}
#[derive(Debug, Deserialize, Serialize, Default)]
#[serde(default)]
pub struct ColorConfig {
pub description: TaskDescriptionColorConfig,
pub status: TaskStatusColorConfig,
pub priority: PriorityColorConfig,
pub show_header: ShowHeaderColorConfig,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct TaskDescriptionColorConfig {
pub ongoing: Highlight,
pub todo: Highlight,
pub done: Highlight,
pub cancelled: Highlight,
}
impl Default for TaskDescriptionColorConfig {
fn default() -> Self {
Self {
ongoing: Highlight {
foreground: Some(Color(Col::Black)),
background: Some(Color(Col::BrightGreen)),
style: vec![],
},
todo: Highlight {
foreground: Some(Color(Col::BrightWhite)),
background: Some(Color(Col::Black)),
style: vec![],
},
done: Highlight {
foreground: Some(Color(Col::BrightBlack)),
background: Some(Color(Col::Black)),
style: vec![StyleAttribute::Dimmed],
},
cancelled: Highlight {
foreground: Some(Color(Col::BrightBlack)),
background: Some(Color(Col::Black)),
style: vec![StyleAttribute::Dimmed, StyleAttribute::Strikethrough],
},
}
}
}
#[derive(Debug, Deserialize, Serialize)]
pub struct TaskStatusColorConfig {
pub ongoing: Highlight,
pub todo: Highlight,
pub done: Highlight,
pub cancelled: Highlight,
}
impl Default for TaskStatusColorConfig {
fn default() -> Self {
Self {
ongoing: Highlight {
foreground: Some(Color(Col::Green)),
background: None,
style: vec![StyleAttribute::Bold],
},
todo: Highlight {
foreground: Some(Color(Col::Magenta)),
background: None,
style: vec![StyleAttribute::Bold],
},
done: Highlight {
foreground: Some(Color(Col::BrightBlack)),
background: None,
style: vec![StyleAttribute::Dimmed],
},
cancelled: Highlight {
foreground: Some(Color(Col::BrightRed)),
background: None,
style: vec![StyleAttribute::Dimmed],
},
}
}
}
#[derive(Debug, Deserialize, Serialize)]
pub struct PriorityColorConfig {
pub low: Highlight,
pub medium: Highlight,
pub high: Highlight,
pub critical: Highlight,
}
impl Default for PriorityColorConfig {
fn default() -> Self {
Self {
low: Highlight {
foreground: Some(Color(Col::BrightBlack)),
background: None,
style: vec![StyleAttribute::Dimmed],
},
medium: Highlight {
foreground: Some(Color(Col::Blue)),
background: None,
style: vec![],
},
high: Highlight {
foreground: Some(Color(Col::Red)),
background: None,
style: vec![],
},
critical: Highlight {
foreground: Some(Color(Col::Black)),
background: Some(Color(Col::BrightRed)),
style: vec![],
},
}
}
}
#[derive(Debug, Deserialize, Serialize)]
pub struct ShowHeaderColorConfig(Highlight);
impl Default for ShowHeaderColorConfig {
fn default() -> Self {
Self(Highlight {
foreground: Some(Color(Col::BrightBlack)),
background: None,
style: vec![],
})
}
}
impl Deref for ShowHeaderColorConfig {
type Target = Highlight;
fn deref(&self) -> &Self::Target {
&self.0
}
}
#[derive(Debug, Deserialize, Serialize)]
pub struct Highlight {
pub foreground: Option<Color>,
pub background: Option<Color>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub style: Vec<StyleAttribute>,
}
impl Highlight {
pub fn highlight(&self, input: impl AsRef<str>) -> HighlightedString {
let mut colored: ColoredString = input.as_ref().into();
if let Some(foreground) = &self.foreground {
colored = colored.color(foreground.0);
}
if let Some(background) = &self.background {
colored = colored.on_color(background.0);
}
for s in &self.style {
colored = s.apply_style(colored);
}
HighlightedString(colored)
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct HighlightedString(ColoredString);
impl fmt::Display for HighlightedString {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
self.0.fmt(f)
}
}
#[derive(Debug, PartialEq)]
pub struct Color(pub Col);
impl Color {
pub fn from_hex(hex: impl AsRef<str>) -> Option<Color> {
let hex = hex.as_ref();
let bytes = hex.as_bytes();
let (mut r, mut g, mut b);
if hex.len() == 4 && bytes[0] == b'#' {
let mut h = u16::from_str_radix(&hex[1..], 16).ok()?;
b = (h & 0xf) as _;
b += b << 4;
h >>= 4;
g = (h & 0xf) as _;
g += g << 4;
h >>= 4;
r = (h & 0xf) as _;
r += r << 4;
} else if hex.len() == 7 && bytes[0] == b'#' {
let mut h = u32::from_str_radix(&hex[1..], 16).ok()?;
b = (h & 0xff) as _;
h >>= 8;
g = (h & 0xff) as _;
h >>= 8;
r = (h & 0xff) as _;
} else {
return None;
}
Some(Color(Col::TrueColor { r, g, b }))
}
}
impl<'de> Deserialize<'de> for Color {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct ColorVisitor;
const EXPECTING: &str = "a color name or hexadecimal color";
impl Visitor<'_> for ColorVisitor {
type Value = Color;
fn expecting(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
formatter.write_str(EXPECTING)
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: de::Error,
{
Col::from_str(value)
.ok()
.map(Color)
.or_else(|| Color::from_hex(value))
.ok_or_else(|| {
E::invalid_value(de::Unexpected::Str(value), &EXPECTING)
})
}
}
deserializer.deserialize_str(ColorVisitor)
}
}
impl Serialize for Color {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let true_color;
let clr = match self.0 {
Col::Black => "black",
Col::Red => "red",
Col::Green => "green",
Col::Yellow => "yellow",
Col::Blue => "blue",
Col::Magenta => "magenta",
Col::Cyan => "cyan",
Col::White => "white",
Col::BrightBlack => "bright black",
Col::BrightRed => "bright red",
Col::BrightGreen => "bright green",
Col::BrightYellow => "bright yellow",
Col::BrightBlue => "bright blue",
Col::BrightMagenta => "bright magenta",
Col::BrightCyan => "bright cyan",
Col::BrightWhite => "bright white",
Col::TrueColor { r, g, b } => {
true_color = format!("#{:02x}{:02x}{:02x}", r, g, b);
&true_color
}
};
serializer.serialize_str(clr)
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_test::*;
#[test]
fn color_hex() {
assert_eq!(
Color::from_hex("#123"),
Some(Color(Col::TrueColor {
r: 0x11,
g: 0x22,
b: 0x33
}))
);
assert_eq!(
Color::from_hex("#112234"),
Some(Color(Col::TrueColor {
r: 0x11,
g: 0x22,
b: 0x34
}))
);
}
#[test]
fn color_colored_name() {
let c = Color(Col::White);
assert_tokens(&c, &[Token::Str("white")])
}
#[test]
fn apply_color_options() {
{
let expected = HighlightedString("test".on_black().white().bold());
let opts = Highlight {
background: Some(Color(Col::Black)),
foreground: Some(Color(Col::White)),
style: vec![StyleAttribute::Bold],
};
assert_eq!(expected, opts.highlight("test"));
}
{
let expected = HighlightedString("test".italic().bold());
let opts = Highlight {
background: None,
foreground: None,
style: vec![StyleAttribute::Bold, StyleAttribute::Italic],
};
assert_eq!(expected, opts.highlight("test"));
}
}
}