Skip to content

Commit

Permalink
Merge pull request #455 from AmmarAbouZor/random_color_tags
Browse files Browse the repository at this point in the history
Added: Colored Tags to Journals, generated and assigned automatically
  • Loading branch information
AmmarAbouZor authored Sep 8, 2024
2 parents fba4398 + 2dc9a51 commit b72ab8a
Show file tree
Hide file tree
Showing 5 changed files with 225 additions and 24 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ TUI-Journal is a terminal-based application written in Rust that allows you to w
- Intuitive, responsive and user-friendly text-based user interface (TUI).
- Create, edit, and delete entries easily.
- Edit journal content with the built-in editor or use your favourite terminal text editor from within the app.
- Add custom tags to the journals and use them in the built-in filter.
- Add custom colored tags to the journals and use them in the built-in filter.
- Fuzzy Finder: Locate your desired journal with lightning-fast speed.
- Search functions for journals title and content in the built-in filter.
- Sort the journals based on their date, priority and title.
Expand Down Expand Up @@ -219,6 +219,8 @@ sync_os_clipboard = false # Syncs editor clipboard actions with operating syste
history_limit = 10 # Sets the maximum changes limit for the undo & redo stacks. Use 0 to disable it.
colored_tags = true # Sets if automatically coloring for tags is enabled.
[export]
default_path = "<Absolute_path_to_export_directory>" # Optional default path to export multiple journals or a single journal's content. Falls back to the current directory if not specified.
show_confirmation = true # Show confirmation after successful export.
Expand Down
142 changes: 142 additions & 0 deletions src/app/colored_tags.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
use std::collections::HashMap;

use ratatui::style::Color;

/// Hard coded colors for the tags.
/// Note: the order to pick the colors is from bottom to top because we are popping the colors from
/// the end of the stack.
const TAG_COLORS: &[TagColors] = &[
TagColors::new(Color::Black, Color::LightMagenta),
TagColors::new(Color::Red, Color::Cyan),
TagColors::new(Color::Yellow, Color::Blue),
TagColors::new(Color::Reset, Color::Red),
TagColors::new(Color::Black, Color::LightYellow),
TagColors::new(Color::Reset, Color::DarkGray),
TagColors::new(Color::Black, Color::LightGreen),
TagColors::new(Color::Black, Color::LightRed),
TagColors::new(Color::Black, Color::LightCyan),
];

#[derive(Debug, Clone)]
/// Manages assigning colors to the tags, keeping track on the assigned colors and providing
/// functions to updating them.
pub struct ColoredTagsManager {
tag_colors_map: HashMap<String, TagColors>,
available_colors: Vec<TagColors>,
}

impl ColoredTagsManager {
pub fn new() -> Self {
let available_colors = TAG_COLORS.to_vec();

Self {
tag_colors_map: HashMap::new(),
available_colors,
}
}

/// Updates the tag_color map with the provided tags, removing the not existing tags and
/// assigning colors to the newly added ones.
pub fn update_tags(&mut self, current_tags: Vec<String>) {
// First: Clear the non-existing anymore tags.
let tags_to_remove: Vec<_> = self
.tag_colors_map
.keys()
.filter(|t| !current_tags.contains(t))
.cloned()
.collect();

for tag in tags_to_remove {
let color = self.tag_colors_map.remove(&tag).unwrap();
self.available_colors.push(color)
}

// Second: Add the new tags to the map
for tag in current_tags {
match self.tag_colors_map.entry(tag) {
std::collections::hash_map::Entry::Occupied(_) => {}
std::collections::hash_map::Entry::Vacant(vacant_entry) => {
let color = self.available_colors.pop().unwrap_or_default();
vacant_entry.insert(color);
}
}
}
}

/// Gets the matching color for the giving tag if tag exists.
pub fn get_tag_color(&self, tag: &str) -> Option<TagColors> {
self.tag_colors_map.get(tag).copied()
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
/// Represents the needed colors for a colored tag
pub struct TagColors {
pub foreground: Color,
pub background: Color,
}

impl TagColors {
pub const fn new(foreground: Color, background: Color) -> Self {
Self {
foreground,
background,
}
}
}

#[cfg(test)]
mod tests {

use super::*;

#[test]
fn test_colored_tags() {
const TAG_ONE: &str = "Tag 1";
const TAG_TWO: &str = "Tag 2";
const ADDED_TAG: &str = "Added Tag";

let mut tags = vec![
String::from(TAG_ONE),
String::from(TAG_TWO),
String::from("Tag 3"),
String::from("Tag 4"),
];

let mut manager = ColoredTagsManager::new();
manager.update_tags(tags.clone());

// Ensure all tags have colors.
for tag in tags.iter() {
assert!(manager.get_tag_color(tag).is_some());
}

// Ensure non existing tags are none
assert!(manager.get_tag_color("Non Existing Tag").is_none());

// Keep track on colors before updating.
let tag_one_color = manager.get_tag_color(TAG_ONE).unwrap();
let tag_two_color = manager.get_tag_color(TAG_TWO).unwrap();

// Remove Tag one with changing the order of the tags.
assert_eq!(tags.swap_remove(0), TAG_ONE);

tags.push(ADDED_TAG.into());

manager.update_tags(tags.clone());

// Ensure all current tags have colors.
for tag in tags.iter() {
assert!(manager.get_tag_color(tag).is_some());
}

// Tag one should have no color after remove.
assert!(manager.get_tag_color(TAG_ONE).is_none());

// Tag two color must remain the same after update.
assert_eq!(manager.get_tag_color(TAG_TWO).unwrap(), tag_two_color);

// Added tag should take the color of tag one because we removed it then added the new tag.
assert_eq!(manager.get_tag_color(ADDED_TAG).unwrap(), tag_one_color);
}
}
39 changes: 36 additions & 3 deletions src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use crate::settings::Settings;
use anyhow::{anyhow, bail, Context};
use backend::{DataProvider, EntriesDTO, Entry, EntryDraft};
use chrono::{DateTime, Utc};
use colored_tags::ColoredTagsManager;
use history::{Change, HistoryManager, HistoryStack};
use rayon::prelude::*;
use std::{
Expand All @@ -15,6 +16,7 @@ use std::{
path::PathBuf,
};

mod colored_tags;
mod external_editor;
mod filter;
mod history;
Expand All @@ -30,6 +32,8 @@ pub use runner::run;
pub use runner::HandleInputReturnType;
pub use ui::UIComponents;

pub use colored_tags::TagColors;

pub struct App<D>
where
D: DataProvider,
Expand All @@ -47,6 +51,7 @@ where
state: AppState,
/// Keeps history of the changes on entries, enabling undo & redo operations
history: HistoryManager,
colored_tags: Option<ColoredTagsManager>,
}

impl<D> App<D>
Expand All @@ -58,6 +63,8 @@ where
let selected_entries = HashSet::new();
let filtered_out_entries = HashSet::new();
let history = HistoryManager::new(settings.history_limit);
let colored_tags = settings.colored_tags.then(ColoredTagsManager::new);

Self {
data_provide,
entries,
Expand All @@ -69,6 +76,7 @@ where
filter: None,
state: Default::default(),
history,
colored_tags,
}
}

Expand Down Expand Up @@ -122,6 +130,8 @@ where

self.update_filtered_out_entries();

self.update_colored_tags();

Ok(())
}

Expand Down Expand Up @@ -163,6 +173,7 @@ where

self.sort_entries();
self.update_filtered_out_entries();
self.update_colored_tags();

Ok(entry_id)
}
Expand Down Expand Up @@ -221,6 +232,7 @@ where

self.update_filter();
self.update_filtered_out_entries();
self.update_colored_tags();

Ok(())
}
Expand Down Expand Up @@ -286,6 +298,7 @@ where

self.update_filter();
self.update_filtered_out_entries();
self.update_colored_tags();

Ok(())
}
Expand Down Expand Up @@ -385,6 +398,26 @@ where
}
}

/// Updates the colors tags mapping, assigning colors to new one and removing the non existing
/// tags from the colors map.
fn update_colored_tags(&mut self) {
if self.colored_tags.is_none() {
return;
}

let tags = { self.get_all_tags() };
if let Some(colored_tags) = self.colored_tags.as_mut() {
colored_tags.update_tags(tags);
}
}

/// Gets the matching color for the giving tag if colored tags are enabled and tag exists.
pub fn get_color_for_tag(&self, tag: &str) -> Option<TagColors> {
self.colored_tags
.as_ref()
.and_then(|c| c.get_tag_color(tag))
}

/// Assigns priority to all entries that don't have a priority assigned to
async fn assign_priority_to_entries(&self, priority: u32) -> anyhow::Result<()> {
self.data_provide
Expand Down Expand Up @@ -429,20 +462,20 @@ where
/// Apply undo on entries returning the id of the effected entry.
pub async fn undo(&mut self) -> anyhow::Result<Option<u32>> {
match self.history.pop_undo() {
Some(change) => self.apply_change(change, HistoryStack::Redo).await,
Some(change) => self.apply_history_change(change, HistoryStack::Redo).await,
None => Ok(None),
}
}

/// Apply redo on entries returning the id of the effected entry.
pub async fn redo(&mut self) -> anyhow::Result<Option<u32>> {
match self.history.pop_redo() {
Some(change) => self.apply_change(change, HistoryStack::Undo).await,
Some(change) => self.apply_history_change(change, HistoryStack::Undo).await,
None => Ok(None),
}
}

async fn apply_change(
async fn apply_history_change(
&mut self,
change: Change,
history_target: HistoryStack,
Expand Down
56 changes: 36 additions & 20 deletions src/app/ui/entries_list/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,26 +131,42 @@ impl<'a> EntriesList {

// *** Tags ***
if !entry.tags.is_empty() {
let tags: Vec<String> = entry.tags.iter().map(String::from).collect();
let tag_line = tags.join(" | ");

// Text wrapping
let tag_line =
textwrap::wrap(&tag_line, area.width as usize - LIST_INNER_MARGIN);

lines_count += tag_line.len();

tag_line
.into_iter()
.map(|line| {
Line::from(Span::styled(
line.to_string(),
Style::default()
.fg(Color::LightCyan)
.add_modifier(Modifier::DIM),
))
})
.for_each(|span| spans.push(span));
const TAGS_SEPARATOR: &str = " | ";
let tags_default_style: Style = Style::default()
.fg(Color::LightCyan)
.add_modifier(Modifier::DIM);

let mut added_lines = 1;
spans.push(Line::default());

for tag in entry.tags.iter() {
let mut last_line = spans.last_mut().unwrap();
let allowd_width = area.width as usize - LIST_INNER_MARGIN;
if !last_line.spans.is_empty() {
if last_line.width() + TAGS_SEPARATOR.len() > allowd_width {
added_lines += 1;
spans.push(Line::default());
last_line = spans.last_mut().unwrap();
}
last_line.push_span(Span::styled(TAGS_SEPARATOR, tags_default_style))
}

let style = app
.get_color_for_tag(tag)
.map(|c| Style::default().bg(c.background).fg(c.foreground))
.unwrap_or(tags_default_style);
let span_to_add = Span::styled(tag.to_owned(), style);

if last_line.width() + tag.len() < allowd_width {
last_line.push_span(span_to_add);
} else {
added_lines += 1;
let line = Line::from(span_to_add);
spans.push(line);
}
}

lines_count += added_lines;
}

ListItem::new(spans)
Expand Down
8 changes: 8 additions & 0 deletions src/settings/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ pub struct Settings {
#[serde(default = "default_history_limit")]
/// Set the maximum size of the history stacks (undo & redo) size.
pub history_limit: usize,
#[serde(default = "default_colored_tags")]
pub colored_tags: bool,
}

impl Default for Settings {
Expand All @@ -63,6 +65,7 @@ impl Default for Settings {
scroll_per_page: Default::default(),
sync_os_clipboard: Default::default(),
history_limit: default_history_limit(),
colored_tags: default_colored_tags(),
}
}
}
Expand All @@ -79,6 +82,10 @@ const fn default_history_limit() -> usize {
10
}

const fn default_colored_tags() -> bool {
true
}

impl Settings {
pub async fn new() -> anyhow::Result<Self> {
let settings_path = get_settings_path()?;
Expand Down Expand Up @@ -131,6 +138,7 @@ impl Settings {
scroll_per_page: _,
sync_os_clipboard: _,
history_limit: _,
colored_tags: _,
} = self;

if self.backend_type.is_none() {
Expand Down

0 comments on commit b72ab8a

Please sign in to comment.