From 2078c5e883f8945c554f949354e367e7bddef488 Mon Sep 17 00:00:00 2001 From: Joscha Date: Wed, 22 Dec 2021 15:58:10 +0000 Subject: [PATCH] Color output --- CHANGELOG.md | 3 + Cargo.lock | 12 +++ Cargo.toml | 1 + src/cli/layout/line.rs | 169 ++++++++++++++++++++++++++++++----------- src/cli/print.rs | 116 +++++++++++++++++++--------- 5 files changed, 221 insertions(+), 80 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d12e3d9..42727fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - `REMIND` statement - `MOVE` entries to a different time +### Changed +- Output is now colored + ## 0.1.0 - 2021-12-20 ### Added diff --git a/Cargo.lock b/Cargo.lock index a4a4cfc..5afc4fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -101,6 +101,17 @@ dependencies = [ "vec_map", ] +[[package]] +name = "colored" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd" +dependencies = [ + "atty", + "lazy_static", + "winapi", +] + [[package]] name = "computus" version = "1.0.0" @@ -425,6 +436,7 @@ name = "today" version = "0.1.0" dependencies = [ "chrono", + "colored", "computus", "directories", "pest", diff --git a/Cargo.toml b/Cargo.toml index edc3e45..7a46abe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ edition = "2021" [dependencies] chrono = "0.4.19" +colored="2.0.0" computus = "1.0.0" directories = "4.0.1" pest = "2.1.3" diff --git a/src/cli/layout/line.rs b/src/cli/layout/line.rs index 23adaba..e5e8412 100644 --- a/src/cli/layout/line.rs +++ b/src/cli/layout/line.rs @@ -14,11 +14,39 @@ use crate::files::Files; use super::super::error::{Error, Result}; use super::day::{DayEntry, DayLayout}; +#[derive(Debug, Clone, Copy)] +pub enum SpanStyle { + Solid, + Dashed, + Dotted, +} + +impl SpanStyle { + fn from_indentation(index: usize) -> Self { + match index % 3 { + 0 => Self::Solid, + 1 => Self::Dashed, + 2 => Self::Dotted, + _ => unreachable!(), + } + } +} + #[derive(Debug, Clone, Copy)] pub enum SpanSegment { - Start, - Middle, - End, + Start(SpanStyle), + Middle(SpanStyle), + End(SpanStyle), +} + +impl SpanSegment { + fn style(&self) -> SpanStyle { + match self { + SpanSegment::Start(s) => *s, + SpanSegment::Middle(s) => *s, + SpanSegment::End(s) => *s, + } + } } #[derive(Debug, Clone, Copy)] @@ -28,10 +56,19 @@ pub enum Times { FromTo(Time, Time), } +#[derive(Debug, Clone, Copy)] +pub enum LineKind { + Task, + Done, + Note, + Birthday, +} + pub enum LineEntry { Day { spans: Vec>, date: NaiveDate, + today: bool, }, Now { spans: Vec>, @@ -41,7 +78,9 @@ pub enum LineEntry { number: Option, spans: Vec>, time: Times, + kind: LineKind, text: String, + extra: Option, }, } @@ -80,7 +119,11 @@ impl LineLayout { for day in layout.range.days() { let spans = self.spans_for_line(); - self.line(LineEntry::Day { spans, date: day }); + self.line(LineEntry::Day { + spans, + date: day, + today: day == layout.today, + }); let layout_entries = layout.days.get(&day).expect("got nonexisting day"); for layout_entry in layout_entries { @@ -114,8 +157,10 @@ impl LineLayout { match l_entry { DayEntry::End(i) => { self.stop_span(*i); - let text = Self::format_entry(files, entries, *i); - self.line_entry(Some(*i), Times::Untimed, text); + let entry = &entries[*i]; + let kind = Self::entry_kind(entry); + let text = Self::entry_title(files, entry); + self.line_entry(Some(*i), Times::Untimed, kind, text, None); } DayEntry::Now(t) => self.line(LineEntry::Now { spans: self.spans_for_line(), @@ -123,89 +168,114 @@ impl LineLayout { }), DayEntry::TimedEnd(i, t) => { self.stop_span(*i); - let text = Self::format_entry(files, entries, *i); - self.line_entry(Some(*i), Times::At(*t), text); + let entry = &entries[*i]; + let kind = Self::entry_kind(entry); + let text = Self::entry_title(files, entry); + self.line_entry(Some(*i), Times::At(*t), kind, text, None); } DayEntry::TimedAt(i, t, t2) => { let time = t2 .map(|t2| Times::FromTo(*t, t2)) .unwrap_or_else(|| Times::At(*t)); - let text = Self::format_entry(files, entries, *i); - self.line_entry(Some(*i), time, text); + let entry = &entries[*i]; + let kind = Self::entry_kind(entry); + let text = Self::entry_title(files, entry); + self.line_entry(Some(*i), time, kind, text, None); } DayEntry::TimedStart(i, t) => { self.start_span(*i); - let text = Self::format_entry(files, entries, *i); - self.line_entry(Some(*i), Times::At(*t), text); + let entry = &entries[*i]; + let kind = Self::entry_kind(entry); + let text = Self::entry_title(files, entry); + self.line_entry(Some(*i), Times::At(*t), kind, text, None); } DayEntry::ReminderSince(i, d) => { - let text = Self::format_entry(files, entries, *i); - let text = if *d == 1 { - format!("{} (yesterday)", text) + let entry = &entries[*i]; + let kind = Self::entry_kind(entry); + let text = Self::entry_title(files, entry); + let extra = if *d == 1 { + "yesterday".to_string() } else { - format!("{} ({} days ago)", text, d) + format!("{} days ago", d) }; - self.line_entry(Some(*i), Times::Untimed, text); + self.line_entry(Some(*i), Times::Untimed, kind, text, Some(extra)); } DayEntry::At(i) => { - let text = Self::format_entry(files, entries, *i); - self.line_entry(Some(*i), Times::Untimed, text); + let entry = &entries[*i]; + let kind = Self::entry_kind(entry); + let text = Self::entry_title(files, entry); + self.line_entry(Some(*i), Times::Untimed, kind, text, None); } DayEntry::ReminderWhile(i, d) => { - let text = Self::format_entry(files, entries, *i); + let entry = &entries[*i]; + let kind = Self::entry_kind(entry); + let text = Self::entry_title(files, entry); let plural = if *d == 1 { "" } else { "s" }; - let text = format!("{} ({} day{} left)", text, d, plural); - self.line_entry(Some(*i), Times::Untimed, text); + let extra = format!("{} day{} left", d, plural); + self.line_entry(Some(*i), Times::Untimed, kind, text, Some(extra)); } DayEntry::Undated(i) => { - let text = Self::format_entry(files, entries, *i); - self.line_entry(Some(*i), Times::Untimed, text); + let entry = &entries[*i]; + let kind = Self::entry_kind(entry); + let text = Self::entry_title(files, entry); + self.line_entry(Some(*i), Times::Untimed, kind, text, None); } DayEntry::Start(i) => { self.start_span(*i); - let text = Self::format_entry(files, entries, *i); - self.line_entry(Some(*i), Times::Untimed, text); + let entry = &entries[*i]; + let kind = Self::entry_kind(entry); + let text = Self::entry_title(files, entry); + self.line_entry(Some(*i), Times::Untimed, kind, text, None); } DayEntry::ReminderUntil(i, d) => { - let text = Self::format_entry(files, entries, *i); - let text = if *d == 1 { - format!("{} (tomorrow)", text) + let entry = &entries[*i]; + let kind = Self::entry_kind(entry); + let text = Self::entry_title(files, entry); + let extra = if *d == 1 { + "tomorrow".to_string() } else { - format!("{} (in {} days)", text, d) + format!("in {} days", d) }; - self.line_entry(Some(*i), Times::Untimed, text); + self.line_entry(Some(*i), Times::Untimed, kind, text, Some(extra)); } } } - fn format_entry(files: &Files, entries: &[Entry], index: usize) -> String { - let entry = entries[index]; + fn entry_kind(entry: &Entry) -> LineKind { + match entry.kind { + EntryKind::Task => LineKind::Task, + EntryKind::TaskDone(_) => LineKind::Done, + EntryKind::Note => LineKind::Note, + EntryKind::Birthday(_) => LineKind::Birthday, + } + } + + fn entry_title(files: &Files, entry: &Entry) -> String { let command = files.command(entry.source); match entry.kind { - EntryKind::Task => format!("T {}", command.title()), - EntryKind::TaskDone(_) => format!("D {}", command.title()), - EntryKind::Note => format!("N {}", command.title()), - EntryKind::Birthday(Some(age)) => format!("B {} ({})", command.title(), age), - EntryKind::Birthday(None) => format!("B {}", command.title()), + EntryKind::Birthday(Some(age)) => format!("{} ({})", command.title(), age), + _ => command.title().to_string(), } } fn start_span(&mut self, index: usize) { - for span in self.spans.iter_mut() { + for (i, span) in self.spans.iter_mut().enumerate() { if span.is_none() { - *span = Some((index, SpanSegment::Start)); + let style = SpanStyle::from_indentation(i); + *span = Some((index, SpanSegment::Start(style))); return; } } // Not enough space, we need another column - self.spans.push(Some((index, SpanSegment::Start))); + let style = SpanStyle::from_indentation(self.spans.len()); + self.spans.push(Some((index, SpanSegment::Start(style)))); } fn stop_span(&mut self, index: usize) { for span in self.spans.iter_mut() { match span { - Some((i, s)) if *i == index => *s = SpanSegment::End, + Some((i, s)) if *i == index => *s = SpanSegment::End(s.style()), _ => {} } } @@ -214,8 +284,8 @@ impl LineLayout { fn step_spans(&mut self) { for span in self.spans.iter_mut() { match span { - Some((_, s @ SpanSegment::Start)) => *s = SpanSegment::Middle, - Some((_, SpanSegment::End)) => *span = None, + Some((_, s @ SpanSegment::Start(_))) => *s = SpanSegment::Middle(s.style()), + Some((_, SpanSegment::End(_))) => *span = None, _ => {} } } @@ -233,7 +303,14 @@ impl LineLayout { self.step_spans(); } - fn line_entry(&mut self, index: Option, time: Times, text: String) { + fn line_entry( + &mut self, + index: Option, + time: Times, + kind: LineKind, + text: String, + extra: Option, + ) { let number = match index { Some(index) => Some(match self.numbers.get(&index) { Some(number) => *number, @@ -250,7 +327,9 @@ impl LineLayout { number, spans: self.spans_for_line(), time, + kind, text, + extra, }); } } diff --git a/src/cli/print.rs b/src/cli/print.rs index cd04776..5adcfa6 100644 --- a/src/cli/print.rs +++ b/src/cli/print.rs @@ -1,10 +1,11 @@ use std::cmp; use chrono::{Datelike, NaiveDate}; +use colored::{Color, ColoredString, Colorize}; use crate::files::primitives::{Time, Weekday}; -use super::layout::line::{LineEntry, LineLayout, SpanSegment, Times}; +use super::layout::line::{LineEntry, LineKind, LineLayout, SpanSegment, SpanStyle, Times}; struct ShowLines { num_width: usize, @@ -23,41 +24,59 @@ impl ShowLines { fn display_line(&mut self, line: &LineEntry) { match line { - LineEntry::Day { spans, date } => self.display_line_date(spans, *date), + LineEntry::Day { spans, date, today } => self.display_line_date(spans, *date, *today), LineEntry::Now { spans, time } => self.display_line_now(spans, *time), LineEntry::Entry { number, spans, time, + kind, text, - } => self.display_line_entry(*number, spans, *time, text), + extra, + } => self.display_line_entry(*number, spans, *time, *kind, text, extra), } } - fn display_line_date(&mut self, spans: &[Option], date: NaiveDate) { + fn display_line_date(&mut self, spans: &[Option], date: NaiveDate, today: bool) { let weekday: Weekday = date.weekday().into(); let weekday = weekday.full_name(); - self.push(&format!( - "{:=>nw$}={:=nw$}\n", - "", - Self::display_spans(spans, '='), + + let color = if today { + Color::BrightCyan + } else { + Color::Cyan + }; + + // '=' symbols before the spans start + let p1 = format!("{:=, spans: &[Option], time: Times, + kind: LineKind, text: &str, + extra: &Option, ) { let num = match number { Some(n) => format!("{}", n), None => "".to_string(), }; - let time = match time { - Times::Untimed => "".to_string(), - Times::At(t) => format!("{} ", t), - Times::FromTo(t1, t2) => format!("{}--{} ", t1, t2), - }; - self.push(&format!( - "{:>nw$} {:sw$} {}{}\n", - num, - Self::display_spans(spans, ' '), - time, + "{:>nw$} {} {} {}{}{}\n", + num.bright_black(), + self.display_spans(spans, " ".into()), + Self::display_kind(kind), + Self::display_time(time), text, + Self::display_extra(extra), nw = self.num_width, - sw = self.span_width )) } - fn display_spans(spans: &[Option], empty: char) -> String { + fn display_spans(&self, spans: &[Option], empty: ColoredString) -> String { let mut result = String::new(); - for segment in spans { - result.push(match segment { - Some(SpanSegment::Start) => '┌', - Some(SpanSegment::Middle) => '│', - Some(SpanSegment::End) => '└', - None => empty, - }); + for i in 0..self.span_width { + if let Some(Some(segment)) = spans.get(i) { + let colored_str = match segment { + SpanSegment::Start(_) => "┌".bright_black(), + SpanSegment::Middle(SpanStyle::Solid) => "│".bright_black(), + SpanSegment::Middle(SpanStyle::Dashed) => "╎".bright_black(), + SpanSegment::Middle(SpanStyle::Dotted) => "┊".bright_black(), + SpanSegment::End(_) => "└".bright_black(), + }; + result.push_str(&format!("{}", colored_str)); + } else { + result.push_str(&format!("{}", empty)); + } } result } + fn display_time(time: Times) -> ColoredString { + match time { + Times::Untimed => "".into(), + Times::At(t) => format!("{} ", t).bright_black(), + Times::FromTo(t1, t2) => format!("{}--{} ", t1, t2).bright_black(), + } + } + + fn display_kind(kind: LineKind) -> ColoredString { + match kind { + LineKind::Task => "T".magenta().bold(), + LineKind::Done => "D".green().bold(), + LineKind::Note => "N".blue().bold(), + LineKind::Birthday => "B".yellow().bold(), + } + } + + fn display_extra(extra: &Option) -> ColoredString { + match extra { + None => "".into(), + Some(extra) => format!(" ({})", extra).bright_black(), + } + } + fn push(&mut self, line: &str) { self.result.push_str(line); }