Skip to content

Commit 891a366

Browse files
authored
✨ color theme support relative and absolute date coloring (#1167)
1 parent 3792143 commit 891a366

5 files changed

Lines changed: 235 additions & 32 deletions

File tree

Cargo.lock

Lines changed: 59 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ sys-locale = "0.3"
3636
once_cell = "1.21"
3737
chrono = { version = "0.4", features = ["unstable-locales"] }
3838
chrono-humanize = "0.2"
39+
jiff = "0.2"
3940
# incompatible with v0.1.11
4041
unicode-width = "0.2"
4142
lscolors = "0.21.0"

src/color.rs

Lines changed: 113 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ pub use crate::flags::color::ThemeOption;
77
use crate::git::GitStatus;
88
use crate::print_output;
99
use crate::theme::{Theme, color::ColorTheme};
10+
use jiff::{Span, SpanTotal, Timestamp, ToSpan, Unit};
1011

1112
#[allow(dead_code)]
1213
#[derive(Hash, Debug, Eq, PartialEq, Clone)]
@@ -45,9 +46,8 @@ pub enum Elem {
4546
System,
4647

4748
/// Last Time Modified
48-
DayOld,
49-
HourOld,
50-
Older,
49+
Date(i64),
50+
InvalidDate,
5151

5252
/// User / Group Name
5353
User,
@@ -123,10 +123,6 @@ impl Elem {
123123
Elem::Hidden => theme.attributes.hidden,
124124
Elem::System => theme.attributes.system,
125125

126-
Elem::DayOld => theme.date.day_old,
127-
Elem::HourOld => theme.date.hour_old,
128-
Elem::Older => theme.date.older,
129-
130126
Elem::User => theme.user,
131127
Elem::Group => theme.group,
132128
Elem::NonFile => theme.size.none,
@@ -169,15 +165,30 @@ impl Elem {
169165
Elem::GitStatus {
170166
status: GitStatus::Conflicted,
171167
} => theme.git_status.conflicted,
168+
Elem::Date(_) | Elem::InvalidDate => {
169+
// These are handled in style_default, not here
170+
Color::Blue
171+
}
172172
}
173173
}
174174
}
175175

176176
pub type ColoredString = StyledContent<String>;
177177

178+
/// Unified timestamp-based date color entry
179+
#[derive(Clone, Debug, PartialEq, Eq)]
180+
struct TimestampColorEntry {
181+
timestamp: i64,
182+
color: Color,
183+
}
184+
178185
pub struct Colors {
179186
theme: Option<ColorTheme>,
180187
lscolors: Option<LsColors>,
188+
default_date_color: Color,
189+
/// Sorted timestamp table: all entries (legacy, relative, absolute) converted to timestamps
190+
/// Sorted in ascending order (oldest first)
191+
timestamp_colors: Vec<TimestampColorEntry>,
181192
}
182193

183194
impl Colors {
@@ -209,7 +220,72 @@ impl Colors {
209220
_ => None,
210221
};
211222

212-
Self { theme, lscolors }
223+
let (default_date_color, timestamp_colors) = if let Some(t) = &theme {
224+
let now = Timestamp::now().as_second();
225+
let mut timestamp_entries: Vec<TimestampColorEntry> = Vec::new();
226+
227+
// Convert legacy config to timestamp entries (relative to now)
228+
if let Some(hour_old) = t.date.hour_old {
229+
timestamp_entries.push(TimestampColorEntry {
230+
timestamp: now - 1.hours().total(Unit::Second).unwrap() as i64,
231+
color: hour_old,
232+
});
233+
}
234+
235+
if let Some(day_old) = t.date.day_old {
236+
timestamp_entries.push(TimestampColorEntry {
237+
timestamp: now
238+
- 1.days()
239+
.total(SpanTotal::from(Unit::Second).days_are_24_hours())
240+
.unwrap() as i64,
241+
color: day_old,
242+
});
243+
}
244+
245+
timestamp_entries.push(TimestampColorEntry {
246+
timestamp: i64::MIN,
247+
color: t.date.older,
248+
});
249+
250+
// Convert relative config to timestamp entries
251+
for relative in &t.date.relative {
252+
if let Ok(span) = relative.threshold.parse::<Span>() {
253+
if let Ok(total_seconds) =
254+
span.total(SpanTotal::from(Unit::Second).days_are_24_hours())
255+
{
256+
let timestamp = now - total_seconds as i64;
257+
timestamp_entries.push(TimestampColorEntry {
258+
timestamp,
259+
color: relative.color,
260+
})
261+
}
262+
}
263+
}
264+
265+
// Convert absolute config to timestamp entries
266+
for absolute in &t.date.absolute {
267+
if let Ok(threshold) = absolute.threshold.parse::<Timestamp>() {
268+
timestamp_entries.push(TimestampColorEntry {
269+
timestamp: threshold.as_second(),
270+
color: absolute.color,
271+
});
272+
}
273+
}
274+
275+
// Sort by timestamp (descending order - newest first)
276+
timestamp_entries.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
277+
278+
(t.date.older, timestamp_entries)
279+
} else {
280+
(Color::Blue, Vec::new())
281+
};
282+
283+
Self {
284+
theme,
285+
lscolors,
286+
default_date_color,
287+
timestamp_colors,
288+
}
213289
}
214290

215291
pub fn colorize<S: Into<String>>(&self, input: S, elem: &Elem) -> ColoredString {
@@ -250,7 +326,30 @@ impl Colors {
250326

251327
fn style_default(&self, elem: &Elem) -> ContentStyle {
252328
if let Some(t) = &self.theme {
253-
let style_fg = ContentStyle::default().with(elem.get_color(t));
329+
let color = match elem {
330+
Elem::Date(timestamp) => {
331+
let mut color = self.default_date_color;
332+
333+
for entry in &self.timestamp_colors {
334+
if *timestamp >= entry.timestamp {
335+
color = entry.color;
336+
break;
337+
}
338+
}
339+
340+
color
341+
}
342+
Elem::InvalidDate => {
343+
// For invalid dates, use the oldest color if available, otherwise default
344+
self.timestamp_colors
345+
.last()
346+
.map(|e| e.color)
347+
.unwrap_or(self.default_date_color)
348+
}
349+
_ => elem.get_color(t),
350+
};
351+
352+
let style_fg = ContentStyle::default().with(color);
254353
if elem.has_suid() {
255354
style_fg.on(Color::AnsiValue(124)) // Red3
256355
} else {
@@ -439,9 +538,11 @@ mod elem {
439538
special: Color::AnsiValue(44), // DarkTurquoise
440539
},
441540
date: color::Date {
442-
hour_old: Color::AnsiValue(40), // Green3
443-
day_old: Color::AnsiValue(42), // SpringGreen2
444-
older: Color::AnsiValue(36), // DarkCyan
541+
hour_old: Some(Color::AnsiValue(40)), // Green3
542+
day_old: Some(Color::AnsiValue(42)), // SpringGreen2
543+
older: Color::AnsiValue(36), // DarkCyan
544+
relative: Vec::new(),
545+
absolute: Vec::new(),
445546
},
446547
size: color::Size {
447548
none: Color::AnsiValue(245), // Grey

src/meta/date.rs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,16 +33,14 @@ impl From<&Metadata> for Date {
3333

3434
impl Date {
3535
pub fn render(&self, colors: &Colors, flags: &Flags) -> ColoredString {
36-
let now = Local::now();
37-
#[allow(deprecated)]
36+
let date_string = self.date_string(flags);
3837
let elem = match self {
39-
&Date::Date(modified) if modified > now - Duration::hours(1) => Elem::HourOld,
40-
&Date::Date(modified) if modified > now - Duration::days(1) => Elem::DayOld,
41-
&Date::Date(_) | Date::Invalid => Elem::Older,
38+
Self::Date(modified) => Elem::Date(modified.timestamp()),
39+
Self::Invalid => Elem::InvalidDate,
4240
};
43-
colors.colorize(self.date_string(flags), &elem)
44-
}
4541

42+
colors.colorize(date_string, &elem)
43+
}
4644
fn date_string(&self, flags: &Flags) -> String {
4745
let locale = current_locale();
4846

0 commit comments

Comments
 (0)