From c5f3d7dfc61b0a70c0becf728d2785829740a39d Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Sun, 28 Sep 2025 12:18:03 +0200 Subject: [PATCH] git_ui: Color blame timestamps by their age --- crates/git_ui/src/blame_ui.rs | 37 +++++++++++++------ crates/settings/src/settings_content/theme.rs | 8 ++++ crates/theme/src/default_colors.rs | 4 ++ crates/theme/src/fallback_themes.rs | 2 + crates/theme/src/schema.rs | 8 ++++ crates/theme/src/styles/colors.rs | 5 +++ 6 files changed, 53 insertions(+), 11 deletions(-) diff --git a/crates/git_ui/src/blame_ui.rs b/crates/git_ui/src/blame_ui.rs index ec405c809518a1959fc07a0488bbfdc5cc14125f..321ac17d081d0e9f45c72e3c82748b1ad65d6d24 100644 --- a/crates/git_ui/src/blame_ui.rs +++ b/crates/git_ui/src/blame_ui.rs @@ -15,7 +15,7 @@ use markdown::{Markdown, MarkdownElement}; use project::{git_store::Repository, project_settings::ProjectSettings}; use settings::Settings as _; use theme::ThemeSettings; -use time::OffsetDateTime; +use time::{OffsetDateTime, ext::NumericalDuration}; use time_format::format_local_timestamp; use ui::{ContextMenu, Divider, IconButtonShape, prelude::*, tooltip_container}; use workspace::Workspace; @@ -42,7 +42,7 @@ impl BlameRenderer for GitBlameRenderer { window: &mut Window, cx: &mut App, ) -> Option { - let relative_timestamp = blame_entry_relative_timestamp(&blame_entry); + let (time_color, relative_timestamp) = blame_entry_relative_timestamp(cx, &blame_entry); let short_commit_id = blame_entry.sha.display_short(); let author_name = blame_entry.author.as_deref().unwrap_or(""); let name = util::truncate_and_trailoff(author_name, GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED); @@ -80,7 +80,7 @@ impl BlameRenderer for GitBlameRenderer { .children(avatar) .child(name), ) - .child(relative_timestamp) + .child(div().text_color(time_color).child(relative_timestamp)) .hover(|style| style.bg(cx.theme().colors().element_hover)) .cursor_pointer() .on_mouse_down(MouseButton::Right, { @@ -150,7 +150,7 @@ impl BlameRenderer for GitBlameRenderer { blame_entry: BlameEntry, cx: &mut App, ) -> Option { - let relative_timestamp = blame_entry_relative_timestamp(&blame_entry); + let (_, relative_timestamp) = blame_entry_relative_timestamp(cx, &blame_entry); let author = blame_entry.author.as_deref().unwrap_or_default(); let summary_enabled = ProjectSettings::get_global(cx) .git @@ -415,17 +415,32 @@ fn deploy_blame_entry_context_menu( }); } -fn blame_entry_relative_timestamp(blame_entry: &BlameEntry) -> String { +fn blame_entry_relative_timestamp(cx: &App, blame_entry: &BlameEntry) -> (Hsla, String) { match blame_entry.author_offset_date_time() { Ok(timestamp) => { + let mut color = cx.theme().colors().version_control_blame_age_new; let local = chrono::Local::now().offset().local_minus_utc(); - time_format::format_localized_timestamp( - timestamp, - time::OffsetDateTime::now_utc(), - time::UtcOffset::from_whole_seconds(local).unwrap(), - time_format::TimestampFormat::Relative, + let now = time::OffsetDateTime::now_utc(); + + // Gradient over ~2 years + let recency = ((now - timestamp - 2.hours()).as_seconds_f32() + / 106.weeks().as_seconds_f32()) + .clamp(0.0, 1.0); + // Sqrt to make more recent colors change more strongly + color.l *= 1. - 0.45 * recency.sqrt(); + ( + color, + time_format::format_localized_timestamp( + timestamp, + now, + time::UtcOffset::from_whole_seconds(local).unwrap(), + time_format::TimestampFormat::Relative, + ), ) } - Err(_) => "Error parsing date".to_string(), + Err(_) => ( + cx.theme().colors().terminal_ansi_bright_red, + "Error parsing date".to_string(), + ), } } diff --git a/crates/settings/src/settings_content/theme.rs b/crates/settings/src/settings_content/theme.rs index bc6eed96ad12ef5f1358e7a21a4fe2db8a6a9d10..ddb0c846ac86cbde5f4107fa86e333aca5873b54 100644 --- a/crates/settings/src/settings_content/theme.rs +++ b/crates/settings/src/settings_content/theme.rs @@ -777,6 +777,14 @@ pub struct ThemeColorsContent { /// Deprecated in favor of `version_control_conflict_marker_theirs`. #[deprecated] pub version_control_conflict_theirs_background: Option, + + /// Blame gutter color for new revisions. + #[serde(rename = "version_control.blame.age_new")] + pub version_control_blame_age_new: Option, + + /// Blame gutter color for old revisions. + #[serde(rename = "version_control.blame.age_old")] + pub version_control_blame_age_old: Option, } #[skip_serializing_none] diff --git a/crates/theme/src/default_colors.rs b/crates/theme/src/default_colors.rs index 051b7acf102597b6f11581afdd45611b9a4b76e3..3f5c940a8f71556a4aaf3b3e1bd5cc17bd8499dc 100644 --- a/crates/theme/src/default_colors.rs +++ b/crates/theme/src/default_colors.rs @@ -154,6 +154,8 @@ impl ThemeColors { version_control_ignored: gray().light().step_12(), version_control_conflict_marker_ours: green().light().step_10().alpha(0.5), version_control_conflict_marker_theirs: blue().light().step_10().alpha(0.5), + version_control_blame_age_new: orange().light().step_10(), + version_control_blame_age_old: blue().light().step_10(), } } @@ -280,6 +282,8 @@ impl ThemeColors { version_control_ignored: gray().dark().step_12(), version_control_conflict_marker_ours: green().dark().step_10().alpha(0.5), version_control_conflict_marker_theirs: blue().dark().step_10().alpha(0.5), + version_control_blame_age_new: orange().dark().step_10(), + version_control_blame_age_old: blue().dark().step_10(), } } } diff --git a/crates/theme/src/fallback_themes.rs b/crates/theme/src/fallback_themes.rs index 13786aca57aab50b503da48d0d4f54fdd78b88c2..781453120c7828314ee8db8862a8530663182227 100644 --- a/crates/theme/src/fallback_themes.rs +++ b/crates/theme/src/fallback_themes.rs @@ -233,6 +233,8 @@ pub(crate) fn zed_default_dark() -> Theme { version_control_ignored: crate::gray().light().step_12(), version_control_conflict_marker_ours: crate::green().light().step_12().alpha(0.5), version_control_conflict_marker_theirs: crate::blue().light().step_12().alpha(0.5), + version_control_blame_age_new: crate::orange().dark().step_9(), + version_control_blame_age_old: crate::blue().dark().step_9(), }, status: StatusColors { conflict: yellow, diff --git a/crates/theme/src/schema.rs b/crates/theme/src/schema.rs index 2d7e1ff9d823eae0d48b375592c6d1f91318f472..a259710a0c7993e821fc9ba72c34292defb64926 100644 --- a/crates/theme/src/schema.rs +++ b/crates/theme/src/schema.rs @@ -756,6 +756,14 @@ pub fn theme_colors_refinement( .as_ref() .or(this.version_control_conflict_theirs_background.as_ref()) .and_then(|color| try_parse_color(color).ok()), + version_control_blame_age_new: this + .version_control_blame_age_new + .as_ref() + .and_then(|color| try_parse_color(color).ok()), + version_control_blame_age_old: this + .version_control_blame_age_old + .as_ref() + .and_then(|color| try_parse_color(color).ok()), } } diff --git a/crates/theme/src/styles/colors.rs b/crates/theme/src/styles/colors.rs index 198ad97adb5d964a1d8f62c5bde99d1d5be5adf7..2f6d889242a8cf36c652df4306d192b4ba838f28 100644 --- a/crates/theme/src/styles/colors.rs +++ b/crates/theme/src/styles/colors.rs @@ -286,6 +286,11 @@ pub struct ThemeColors { pub version_control_conflict_marker_ours: Hsla, /// Represents the "theirs" region of a merge conflict. pub version_control_conflict_marker_theirs: Hsla, + + /// Blame gutter color for new revisions. + pub version_control_blame_age_new: Hsla, + /// Blame gutter color for old revisions. + pub version_control_blame_age_old: Hsla, } #[derive(EnumIter, Debug, Clone, Copy, AsRefStr)]