Skip to main content

fancy_tree/config/colors/
mod.rs

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