Skip to content

Commit a8646e6

Browse files
committed
Merge branch 'plugin_update_log'
2 parents fddf6b9 + e16cac7 commit a8646e6

File tree

6 files changed

+393
-11
lines changed

6 files changed

+393
-11
lines changed

Cargo.lock

Lines changed: 149 additions & 0 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
@@ -27,6 +27,7 @@ serde = { version = "1.0.228", features = ["derive"] }
2727
serde_path_to_error = "0.1.16"
2828
thiserror = "2.0.17"
2929
toml_edit = { version = "0.23.7", features = ["serde"] }
30+
timeago = "0.4"
3031
url = "2.5.7"
3132

3233
[dev-dependencies]

src/commands/plugins/ui.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ pub fn format_plugin_errors(
1616
miette::miette!("Some plugins failed to {operation}\n{error_messages}")
1717
}
1818

19+
const OSC8_PREFIX: &str = "\u{1b}]8;;";
20+
const OSC8_SUFFIX: &str = "\u{1b}]8;;\u{1b}\\";
21+
22+
/// Wraps `message` in an OSC 8 hyperlink sequence pointing to `url`.
23+
pub fn hyperlink(message: &str, url: &str) -> String {
24+
format!("{OSC8_PREFIX}{url}\u{1b}\\{message}{OSC8_SUFFIX}")
25+
}
26+
1927
#[derive(Debug, Clone, Copy)]
2028
enum PluginSpinnerResult {
2129
AlreadyInstalled,
@@ -91,3 +99,25 @@ impl PluginSpinner {
9199
self.pb.finish_with_message(message);
92100
}
93101
}
102+
103+
#[cfg(test)]
104+
mod tests {
105+
use super::*;
106+
107+
#[test]
108+
fn hyperlink_wraps_message_with_osc8_sequence() {
109+
let message = "hash";
110+
let url = "https://example.com/commit";
111+
let expected = format!("{OSC8_PREFIX}{url}\u{1b}\\{message}{OSC8_SUFFIX}");
112+
113+
assert_eq!(hyperlink(message, url), expected);
114+
}
115+
116+
#[test]
117+
fn hyperlink_allows_empty_message() {
118+
let url = "https://example.com";
119+
let expected = format!("{OSC8_PREFIX}{url}\u{1b}\\{OSC8_SUFFIX}");
120+
121+
assert_eq!(hyperlink("", url), expected);
122+
}
123+
}

src/commands/plugins/update.rs

Lines changed: 75 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,14 @@
11
use std::sync::Mutex;
22
use std::thread;
3+
use std::time::{Duration, SystemTime};
34

45
use indicatif::MultiProgress;
56
use miette::Result;
67
use owo_colors::OwoColorize;
8+
use timeago::Formatter;
79

810
use super::ui::{self, PluginSpinner};
9-
use crate::muxi::{PluginUpdateStatus, Settings};
11+
use crate::muxi::{PluginChange, PluginUpdateStatus, Settings};
1012

1113
pub fn update() -> Result<()> {
1214
let plugins = Settings::from_lua()?.plugins;
@@ -18,19 +20,40 @@ pub fn update() -> Result<()> {
1820

1921
let multi = MultiProgress::new();
2022
let errors = Mutex::new(Vec::new());
23+
let change_logs = Mutex::new(Vec::new());
2124

2225
thread::scope(|s| {
23-
for plugin in plugins {
24-
s.spawn(|| {
25-
let spinner = PluginSpinner::new(&multi, &plugin.name);
26+
for (index, plugin) in plugins.into_iter().enumerate() {
27+
let progress = &multi;
28+
let errors_ref = &errors;
29+
let change_logs_ref = &change_logs;
30+
31+
s.spawn(move || {
32+
let spinner = PluginSpinner::new(progress, &plugin.name);
33+
let plugin_name = plugin.name.clone();
2634

2735
match plugin.update() {
28-
Ok(PluginUpdateStatus::Updated { from, to }) => {
29-
let detail = match from {
36+
Ok(PluginUpdateStatus::Updated {
37+
from,
38+
to,
39+
changes,
40+
range_url,
41+
}) => {
42+
let display = match from {
3043
Some(from) => format!("{from}..{to}"),
3144
None => to,
3245
};
46+
let detail = if let Some(url) = range_url.as_ref() {
47+
ui::hyperlink(&display, url)
48+
} else {
49+
display
50+
};
3351
spinner.finish_success(Some(&detail));
52+
53+
if !changes.is_empty() {
54+
let log = format_plugin_changes(&plugin_name, &changes);
55+
change_logs_ref.lock().unwrap().push((index, log));
56+
}
3457
}
3558
Ok(PluginUpdateStatus::UpToDate { commit }) => {
3659
spinner.finish_up_to_date(Some(&commit));
@@ -40,17 +63,62 @@ pub fn update() -> Result<()> {
4063
}
4164
Err(error) => {
4265
spinner.finish_error();
43-
errors.lock().unwrap().push((plugin, error));
66+
errors_ref.lock().unwrap().push((plugin.clone(), error));
4467
}
4568
}
4669
});
4770
}
4871
});
4972

73+
drop(multi);
74+
75+
let mut change_logs = change_logs.into_inner().unwrap();
76+
change_logs.sort_by_key(|(index, _)| *index);
77+
if !change_logs.is_empty() {
78+
println!();
79+
for (_, log) in change_logs {
80+
println!("{log}");
81+
}
82+
}
83+
5084
let errors = errors.into_inner().unwrap();
5185
if errors.is_empty() {
5286
Ok(())
5387
} else {
5488
Err(ui::format_plugin_errors(&errors, "update"))
5589
}
5690
}
91+
92+
fn format_plugin_changes(plugin_name: &str, changes: &[PluginChange]) -> String {
93+
let header = plugin_name.bold().to_string();
94+
95+
let body = changes
96+
.iter()
97+
.map(|change| {
98+
let id_colored = change.id.green().bold().to_string();
99+
let id_formatted = change
100+
.url
101+
.as_ref()
102+
.map_or_else(|| id_colored.clone(), |url| ui::hyperlink(&id_colored, url));
103+
104+
format!(
105+
" {} {} {}",
106+
id_formatted,
107+
change.summary.trim(),
108+
format!("({})", format_relative_time(change.time)).dimmed()
109+
)
110+
})
111+
.collect::<Vec<_>>()
112+
.join("\n");
113+
114+
format!("{header}\n{body}")
115+
}
116+
117+
fn format_relative_time(time: SystemTime) -> String {
118+
let now = SystemTime::now();
119+
let duration = now
120+
.duration_since(time)
121+
.unwrap_or_else(|_| Duration::from_secs(0));
122+
123+
Formatter::new().convert(duration)
124+
}

0 commit comments

Comments
 (0)