Skip to main content

fancy_tree/config/colors/
mod.rs

1//! Module for configuring colors.
2use super::ConfigFile;
3use crate::color::Color;
4use crate::git::status::{self, Status};
5use crate::lua::interop;
6use crate::tree::{
7    Entry,
8    entry::{Attributes, attributes::FileAttributes},
9};
10use mlua::{FromLua, Lua};
11use owo_colors::AnsiColors;
12use std::path::Path;
13
14/// The configuration for application colors.
15#[derive(Debug, Default)]
16pub struct Colors {
17    /// Function to get the color for an entry's icon.
18    for_icon: Option<mlua::Function>,
19    git_statuses: GitStatuses,
20}
21
22impl Colors {
23    /// The default color to use for files.
24    const DEFAULT_FILE_COLOR: Option<Color> = None;
25    /// The default color to use when a file is an executable.
26    const DEFAULT_EXECUTABLE_COLOR: Option<Color> = Some(Color::Ansi(AnsiColors::Green));
27    /// The default color to use for directories/folders.
28    const DEFAULT_DIRECTORY_COLOR: Option<Color> = Some(Color::Ansi(AnsiColors::Blue));
29    /// The default color to use for symlinks.
30    const DEFAULT_SYMLINK_COLOR: Option<Color> = Some(Color::Ansi(AnsiColors::Cyan));
31
32    /// Get the color for an entry's icon.
33    pub fn for_icon<P>(&self, entry: &Entry<P>) -> Option<Color>
34    where
35        P: AsRef<Path>,
36    {
37        let path = entry.path();
38        let default: Option<Color> = match entry.attributes() {
39            Attributes::Directory(_) => Self::DEFAULT_DIRECTORY_COLOR,
40            Attributes::File(attributes) => Self::get_file_color(attributes),
41            Attributes::Symlink(_) => Self::DEFAULT_SYMLINK_COLOR,
42        };
43        let attributes = interop::FileAttributes::from(entry);
44
45        // TODO Report error
46        self.for_icon
47            .as_ref()
48            .map_or(Ok(default), |f| {
49                f.call::<Option<Color>>((path, attributes, default))
50            })
51            .unwrap_or(default)
52    }
53
54    /// Get the color for an untracked file's status.
55    pub fn for_untracked_git_status(&self, status: Status) -> Option<Color> {
56        self.git_statuses.get_untracked_color(status)
57    }
58
59    /// Get the color for an tracked file's status.
60    pub fn for_tracked_git_status(&self, status: Status) -> Option<Color> {
61        self.git_statuses.get_tracked_color(status)
62    }
63
64    /// Gets the color for a file.
65    fn get_file_color(attributes: &FileAttributes) -> Option<Color> {
66        attributes
67            .language()
68            .map(|language| language.rgb())
69            .map(|(r, g, b)| Color::Rgb(r, g, b))
70            .or_else(|| {
71                attributes
72                    .is_executable()
73                    .then_some(Self::DEFAULT_EXECUTABLE_COLOR)
74                    .flatten()
75            })
76            .or(Self::DEFAULT_FILE_COLOR)
77    }
78}
79
80impl ConfigFile for Colors {
81    const FILENAME: &'static str = "colors.lua";
82    const DEFAULT_MODULE: &'static str = include_str!("./colors.lua");
83}
84
85impl FromLua for Colors {
86    fn from_lua(value: mlua::Value, lua: &Lua) -> mlua::Result<Self> {
87        const FOR_ICON_KEY: &str = "icons";
88        const GIT_STATUSES_KEY: &str = "git_statuses";
89
90        let table = mlua::Table::from_lua(value, lua)?;
91        let for_icon = table.get(FOR_ICON_KEY)?;
92        let git_statuses = table
93            .get::<Option<GitStatuses>>(GIT_STATUSES_KEY)?
94            .unwrap_or_default();
95
96        let colors = Self {
97            for_icon,
98            git_statuses,
99        };
100        Ok(colors)
101    }
102}
103
104/// The configuration for git status colors.
105#[derive(Debug, Default)]
106struct GitStatuses {
107    /// Function to get the color for tracked statuses.
108    tracked: Option<mlua::Function>,
109    /// Function to get the color for untracked statuses.
110    untracked: Option<mlua::Function>,
111}
112
113impl GitStatuses {
114    /// Gets the default color for a git status.
115    const fn get_default_color<S>(status: Status) -> Option<Color>
116    where
117        S: StatusColor,
118    {
119        let color = match status {
120            Status::Added => S::DEFAULT_ADDED,
121            Status::Modified => S::DEFAULT_MODIFIED,
122            Status::Removed => S::DEFAULT_REMOVED,
123            Status::Renamed => S::DEFAULT_RENAMED,
124        };
125        Some(Color::Ansi(color))
126    }
127
128    /// Gets the color for a tracked git status.
129    fn get_tracked_color(&self, status: Status) -> Option<Color> {
130        let default = Self::get_default_color::<status::Tracked>(status);
131        // TODO Report error
132        self.tracked.as_ref().map_or(default, |f| {
133            f.call::<Option<Color>>((status, default))
134                .unwrap_or(default)
135        })
136    }
137
138    /// Gets the color for an untracked git status.
139    fn get_untracked_color(&self, status: Status) -> Option<Color> {
140        let default = Self::get_default_color::<status::Untracked>(status);
141        // TODO Report error
142        self.untracked.as_ref().map_or(default, |f| {
143            f.call::<Option<Color>>((status, default))
144                .unwrap_or(default)
145        })
146    }
147}
148
149impl FromLua for GitStatuses {
150    fn from_lua(value: mlua::Value, lua: &Lua) -> mlua::Result<Self> {
151        const TRACKED_KEY: &str = "tracked";
152        const UNTRACKED_KEY: &str = "untracked";
153
154        let table = mlua::Table::from_lua(value, lua)?;
155        let tracked = table.get(TRACKED_KEY)?;
156        let untracked = table.get(UNTRACKED_KEY)?;
157
158        let git_statuses = Self { tracked, untracked };
159        Ok(git_statuses)
160    }
161}
162
163/// Private trait to generalize getting the color for a status.
164trait StatusColor {
165    /// Default color for added status.
166    const DEFAULT_ADDED: AnsiColors;
167    /// Default color for modified status.
168    const DEFAULT_MODIFIED: AnsiColors;
169    /// Default color for removed status.
170    const DEFAULT_REMOVED: AnsiColors;
171    /// Default color for renamed status.
172    const DEFAULT_RENAMED: AnsiColors;
173}
174
175impl StatusColor for status::Tracked {
176    const DEFAULT_ADDED: AnsiColors = AnsiColors::Green;
177    const DEFAULT_MODIFIED: AnsiColors = AnsiColors::Yellow;
178    const DEFAULT_REMOVED: AnsiColors = AnsiColors::Red;
179    const DEFAULT_RENAMED: AnsiColors = AnsiColors::Cyan;
180}
181
182impl StatusColor for status::Untracked {
183    const DEFAULT_ADDED: AnsiColors = AnsiColors::BrightGreen;
184    const DEFAULT_MODIFIED: AnsiColors = AnsiColors::BrightYellow;
185    const DEFAULT_REMOVED: AnsiColors = AnsiColors::BrightRed;
186    const DEFAULT_RENAMED: AnsiColors = AnsiColors::BrightCyan;
187}