Improve tooltip
This commit is contained in:
1 parent
c312d83c7a
commit
81ec07a9aa
1 file changed
+136
-49
+136
-49
@@ -7,17 +7,18 @@ use crate::model::{Provider, ProviderState, UsageSnapshot, UsageWindow};
|
||||
pub struct WaybarView {
|
||||
pub text: String,
|
||||
pub tooltip: String,
|
||||
pub tooltip_markup: String,
|
||||
pub class: String,
|
||||
}
|
||||
|
||||
pub fn render(snapshot: &UsageSnapshot, now: DateTime<Utc>, enabled: &[Provider]) -> WaybarView {
|
||||
let mut parts = Vec::new();
|
||||
let mut tooltip = Vec::new();
|
||||
let mut blocks = Vec::new();
|
||||
let mut worst = "unknown";
|
||||
let mut severity = level_rank("unknown");
|
||||
let mut any = false;
|
||||
if snapshot.is_demo {
|
||||
tooltip.push("DEMO".to_string());
|
||||
blocks.push(("DEMO".to_string(), "<b>DEMO</b>".to_string()));
|
||||
}
|
||||
for provider in Provider::ALL {
|
||||
if !enabled.contains(&provider) {
|
||||
@@ -43,21 +44,24 @@ pub fn render(snapshot: &UsageSnapshot, now: DateTime<Utc>, enabled: &[Provider]
|
||||
severity = level_rank(provider_level);
|
||||
worst = provider_level;
|
||||
}
|
||||
tooltip.push(provider_block(provider, &state, snapshot.show_used_percentage, now));
|
||||
blocks.push(provider_block(provider, &state, snapshot.show_used_percentage, now));
|
||||
}
|
||||
if !any {
|
||||
let tooltip = "Every provider is disabled.".to_string();
|
||||
return WaybarView {
|
||||
text: "AI usage off".to_string(),
|
||||
tooltip: "Every provider is disabled.".to_string(),
|
||||
tooltip: tooltip.clone(),
|
||||
tooltip_markup: tooltip,
|
||||
class: "unknown".to_string(),
|
||||
};
|
||||
}
|
||||
if tooltip.iter().all(|line| line.contains("Awaiting first update")) && parts.iter().all(|part| part.ends_with("—")) {
|
||||
if blocks.iter().all(|(plain, _)| plain.contains("Awaiting first update")) && parts.iter().all(|part| part.ends_with("—")) {
|
||||
worst = "unknown";
|
||||
}
|
||||
WaybarView {
|
||||
text: parts.join(" "),
|
||||
tooltip: tooltip.join("\n"),
|
||||
tooltip: blocks.iter().map(|(plain, _)| plain.as_str()).collect::<Vec<_>>().join("\n\n"),
|
||||
tooltip_markup: blocks.iter().map(|(_, markup)| markup.as_str()).collect::<Vec<_>>().join("\n\n"),
|
||||
class: worst.to_string(),
|
||||
}
|
||||
}
|
||||
@@ -65,7 +69,7 @@ pub fn render(snapshot: &UsageSnapshot, now: DateTime<Utc>, enabled: &[Provider]
|
||||
pub fn json(view: &WaybarView) -> String {
|
||||
serde_json::json!({
|
||||
"text": view.text,
|
||||
"tooltip": view.tooltip,
|
||||
"tooltip": view.tooltip_markup,
|
||||
"class": view.class,
|
||||
})
|
||||
.to_string()
|
||||
@@ -96,60 +100,137 @@ fn displayed_window(provider: Provider, state: &ProviderState) -> Option<UsageWi
|
||||
}
|
||||
}
|
||||
|
||||
fn provider_block(provider: Provider, state: &ProviderState, show_used: bool, now: DateTime<Utc>) -> String {
|
||||
let mut lines = vec![provider.display_name().to_string()];
|
||||
if state.is_stale(now) {
|
||||
lines.push("Stale".to_string());
|
||||
}
|
||||
fn provider_block(provider: Provider, state: &ProviderState, show_used: bool, now: DateTime<Utc>) -> (String, String) {
|
||||
let usage = state.last_successful_usage.as_ref();
|
||||
let fetched = usage.map(|usage| formatting::clock(Some(usage.fetched_at)));
|
||||
let mut plain = vec![header_plain(provider.display_name(), state.is_stale(now), fetched.as_deref())];
|
||||
let mut markup = vec![header_markup(provider.display_name(), state.is_stale(now), fetched.as_deref())];
|
||||
if let Some(message) = &state.message {
|
||||
lines.push(message.clone());
|
||||
plain.push(format!(" {message}"));
|
||||
markup.push(format!(" {}", pango_escape(message)));
|
||||
}
|
||||
let Some(usage) = &state.last_successful_usage else {
|
||||
lines.push(state.status.label().to_string());
|
||||
return lines.join("\n");
|
||||
let Some(usage) = usage else {
|
||||
let label = state.status.label();
|
||||
plain.push(format!(" {label}"));
|
||||
markup.push(format!(" {}", pango_escape(label)));
|
||||
return (plain.join("\n"), markup.join("\n"));
|
||||
};
|
||||
if provider == Provider::Cursor {
|
||||
for pool in usage.display_billing_pools() {
|
||||
lines.push(format!(
|
||||
"{} {}",
|
||||
pool.name,
|
||||
formatting::percentage(Some(&pool.window), show_used)
|
||||
));
|
||||
}
|
||||
if usage.display_billing_pools().is_empty() {
|
||||
let pools = usage.display_billing_pools();
|
||||
if pools.is_empty() {
|
||||
if let Some(window) = &usage.billing_window {
|
||||
lines.push(format!("Included {}", formatting::percentage(Some(window), show_used)));
|
||||
}
|
||||
}
|
||||
lines.push(
|
||||
usage
|
||||
.billing_reset_label
|
||||
.as_ref()
|
||||
.map(|label| format!("Resets {label}"))
|
||||
.unwrap_or_else(|| "Reset unknown".to_string()),
|
||||
);
|
||||
for metric in usage.display_additional_usage() {
|
||||
lines.push(format!("{}: {}", metric.name, metric.value));
|
||||
let (row_plain, row_markup) = metric_row("Included", Some(window), show_used, None);
|
||||
plain.push(row_plain);
|
||||
markup.push(row_markup);
|
||||
}
|
||||
} else {
|
||||
lines.push(window_line("5h", usage.short_window.as_ref(), show_used, now));
|
||||
lines.push(window_line("Week", usage.weekly_window.as_ref(), show_used, now));
|
||||
for pool in pools {
|
||||
let (row_plain, row_markup) = metric_row(&pool.name, Some(&pool.window), show_used, None);
|
||||
plain.push(row_plain);
|
||||
markup.push(row_markup);
|
||||
}
|
||||
}
|
||||
if let Some(label) = &usage.billing_reset_label {
|
||||
let line = format!("Resets {label}");
|
||||
plain.push(format!(" {line}"));
|
||||
markup.push(format!(" <span foreground=\"#6c7086\">{}</span>", pango_escape(&line)));
|
||||
}
|
||||
for metric in usage.display_additional_usage() {
|
||||
let line = format!("{}: {}", metric.name, metric.value);
|
||||
plain.push(format!(" {line}"));
|
||||
markup.push(format!(" {}", pango_escape(&line)));
|
||||
}
|
||||
} else {
|
||||
for (label, window) in [("5h", usage.short_window.as_ref()), ("Week", usage.weekly_window.as_ref())] {
|
||||
let reset = reset_clause(window, now);
|
||||
let (row_plain, row_markup) = metric_row(label, window, show_used, reset.as_deref());
|
||||
plain.push(row_plain);
|
||||
markup.push(row_markup);
|
||||
}
|
||||
for metric in &usage.additional_usage {
|
||||
lines.push(format!("{}: {}", metric.name, metric.value));
|
||||
let line = format!("{}: {}", metric.name, metric.value);
|
||||
plain.push(format!(" {line}"));
|
||||
markup.push(format!(" {}", pango_escape(&line)));
|
||||
}
|
||||
}
|
||||
lines.push(format!("Updated {}", formatting::clock(Some(usage.fetched_at))));
|
||||
lines.join("\n")
|
||||
(plain.join("\n"), markup.join("\n"))
|
||||
}
|
||||
|
||||
fn window_line(label: &str, window: Option<&UsageWindow>, show_used: bool, now: DateTime<Utc>) -> String {
|
||||
let percent = formatting::percentage(window, show_used);
|
||||
let reset = window
|
||||
.and_then(|window| window.reset_at)
|
||||
.map(|date| formatting::reset(Some(date), now, true))
|
||||
.unwrap_or_else(|| "—".to_string());
|
||||
let relative = formatting::relative_reset(window.and_then(|window| window.reset_at), now);
|
||||
format!("{label} {percent} {reset} {relative}")
|
||||
fn header_plain(name: &str, stale: bool, fetched: Option<&str>) -> String {
|
||||
let suffix = header_suffix(stale, fetched);
|
||||
if suffix.is_empty() {
|
||||
name.to_string()
|
||||
} else {
|
||||
format!("{name} {suffix}")
|
||||
}
|
||||
}
|
||||
|
||||
fn header_markup(name: &str, stale: bool, fetched: Option<&str>) -> String {
|
||||
let suffix = header_suffix(stale, fetched);
|
||||
if suffix.is_empty() {
|
||||
format!("<b>{}</b>", pango_escape(name))
|
||||
} else {
|
||||
format!(
|
||||
"<b>{}</b> <span foreground=\"#6c7086\">{}</span>",
|
||||
pango_escape(name),
|
||||
pango_escape(&suffix)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn header_suffix(stale: bool, fetched: Option<&str>) -> String {
|
||||
let mut parts = Vec::new();
|
||||
if stale {
|
||||
parts.push("stale");
|
||||
}
|
||||
if let Some(fetched) = fetched {
|
||||
parts.push(fetched);
|
||||
}
|
||||
parts.join(" · ")
|
||||
}
|
||||
|
||||
fn metric_row(label: &str, window: Option<&UsageWindow>, show_used: bool, reset: Option<&str>) -> (String, String) {
|
||||
let percent = match window {
|
||||
Some(window) => formatting::percentage(Some(window), show_used),
|
||||
None => "—".to_string(),
|
||||
};
|
||||
let color = level_color(level(window.and_then(|window| window.remaining_percentage)));
|
||||
let padded = format!("{label:<16}");
|
||||
let mut plain = format!(" {padded}{percent}");
|
||||
let mut markup = format!(
|
||||
" {}<span foreground=\"{color}\">{}</span>",
|
||||
pango_escape(&padded),
|
||||
pango_escape(&percent)
|
||||
);
|
||||
if let Some(reset) = reset {
|
||||
plain.push_str(" ");
|
||||
plain.push_str(reset);
|
||||
markup.push_str(" <span foreground=\"#6c7086\">");
|
||||
markup.push_str(&pango_escape(reset));
|
||||
markup.push_str("</span>");
|
||||
}
|
||||
(plain, markup)
|
||||
}
|
||||
|
||||
fn reset_clause(window: Option<&UsageWindow>, now: DateTime<Utc>) -> Option<String> {
|
||||
let date = window.and_then(|window| window.reset_at)?;
|
||||
let absolute = formatting::reset(Some(date), now, true);
|
||||
let relative = formatting::relative_reset(Some(date), now);
|
||||
let relative = relative.strip_prefix("Reset ").unwrap_or(relative.as_str());
|
||||
Some(format!("{absolute} · {relative}"))
|
||||
}
|
||||
|
||||
fn level_color(class: &str) -> &'static str {
|
||||
match class {
|
||||
"ok" => "#a6e3a1",
|
||||
"warning" => "#fab387",
|
||||
"critical" => "#f38ba8",
|
||||
_ => "#cdd6f4",
|
||||
}
|
||||
}
|
||||
|
||||
fn pango_escape(value: &str) -> String {
|
||||
value.replace('&', "&").replace('<', "<").replace('>', ">")
|
||||
}
|
||||
|
||||
pub fn status_text(snapshot: &UsageSnapshot, active: bool, now: DateTime<Utc>, enabled: &[Provider]) -> String {
|
||||
@@ -186,6 +267,12 @@ mod tests {
|
||||
assert_eq!(view.class, "warning");
|
||||
assert!(view.tooltip.contains("Cursor models"));
|
||||
assert!(view.tooltip.contains("Resets Oct 30"));
|
||||
assert!(view.tooltip.contains("\n\n"));
|
||||
assert!(!view.tooltip.contains("Reset unknown"));
|
||||
let encoded = json(&view);
|
||||
assert!(encoded.contains("foreground=\\\"#fab387\\\""));
|
||||
assert!(encoded.contains("<b>Cursor</b>"));
|
||||
assert!(!view.tooltip.contains("<span"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user