Improve tooltip
This commit is contained in:
1 parent
c312d83c7a
commit
81ec07a9aa
1 file changed
+133
-46
+133
-46
@@ -7,17 +7,18 @@ use crate::model::{Provider, ProviderState, UsageSnapshot, UsageWindow};
|
|||||||
pub struct WaybarView {
|
pub struct WaybarView {
|
||||||
pub text: String,
|
pub text: String,
|
||||||
pub tooltip: String,
|
pub tooltip: String,
|
||||||
|
pub tooltip_markup: String,
|
||||||
pub class: String,
|
pub class: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render(snapshot: &UsageSnapshot, now: DateTime<Utc>, enabled: &[Provider]) -> WaybarView {
|
pub fn render(snapshot: &UsageSnapshot, now: DateTime<Utc>, enabled: &[Provider]) -> WaybarView {
|
||||||
let mut parts = Vec::new();
|
let mut parts = Vec::new();
|
||||||
let mut tooltip = Vec::new();
|
let mut blocks = Vec::new();
|
||||||
let mut worst = "unknown";
|
let mut worst = "unknown";
|
||||||
let mut severity = level_rank("unknown");
|
let mut severity = level_rank("unknown");
|
||||||
let mut any = false;
|
let mut any = false;
|
||||||
if snapshot.is_demo {
|
if snapshot.is_demo {
|
||||||
tooltip.push("DEMO".to_string());
|
blocks.push(("DEMO".to_string(), "<b>DEMO</b>".to_string()));
|
||||||
}
|
}
|
||||||
for provider in Provider::ALL {
|
for provider in Provider::ALL {
|
||||||
if !enabled.contains(&provider) {
|
if !enabled.contains(&provider) {
|
||||||
@@ -43,21 +44,24 @@ pub fn render(snapshot: &UsageSnapshot, now: DateTime<Utc>, enabled: &[Provider]
|
|||||||
severity = level_rank(provider_level);
|
severity = level_rank(provider_level);
|
||||||
worst = 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 {
|
if !any {
|
||||||
|
let tooltip = "Every provider is disabled.".to_string();
|
||||||
return WaybarView {
|
return WaybarView {
|
||||||
text: "AI usage off".to_string(),
|
text: "AI usage off".to_string(),
|
||||||
tooltip: "Every provider is disabled.".to_string(),
|
tooltip: tooltip.clone(),
|
||||||
|
tooltip_markup: tooltip,
|
||||||
class: "unknown".to_string(),
|
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";
|
worst = "unknown";
|
||||||
}
|
}
|
||||||
WaybarView {
|
WaybarView {
|
||||||
text: parts.join(" "),
|
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(),
|
class: worst.to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -65,7 +69,7 @@ pub fn render(snapshot: &UsageSnapshot, now: DateTime<Utc>, enabled: &[Provider]
|
|||||||
pub fn json(view: &WaybarView) -> String {
|
pub fn json(view: &WaybarView) -> String {
|
||||||
serde_json::json!({
|
serde_json::json!({
|
||||||
"text": view.text,
|
"text": view.text,
|
||||||
"tooltip": view.tooltip,
|
"tooltip": view.tooltip_markup,
|
||||||
"class": view.class,
|
"class": view.class,
|
||||||
})
|
})
|
||||||
.to_string()
|
.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 {
|
fn provider_block(provider: Provider, state: &ProviderState, show_used: bool, now: DateTime<Utc>) -> (String, String) {
|
||||||
let mut lines = vec![provider.display_name().to_string()];
|
let usage = state.last_successful_usage.as_ref();
|
||||||
if state.is_stale(now) {
|
let fetched = usage.map(|usage| formatting::clock(Some(usage.fetched_at)));
|
||||||
lines.push("Stale".to_string());
|
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 {
|
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 {
|
let Some(usage) = usage else {
|
||||||
lines.push(state.status.label().to_string());
|
let label = state.status.label();
|
||||||
return lines.join("\n");
|
plain.push(format!(" {label}"));
|
||||||
|
markup.push(format!(" {}", pango_escape(label)));
|
||||||
|
return (plain.join("\n"), markup.join("\n"));
|
||||||
};
|
};
|
||||||
if provider == Provider::Cursor {
|
if provider == Provider::Cursor {
|
||||||
for pool in usage.display_billing_pools() {
|
let pools = usage.display_billing_pools();
|
||||||
lines.push(format!(
|
if pools.is_empty() {
|
||||||
"{} {}",
|
|
||||||
pool.name,
|
|
||||||
formatting::percentage(Some(&pool.window), show_used)
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if usage.display_billing_pools().is_empty() {
|
|
||||||
if let Some(window) = &usage.billing_window {
|
if let Some(window) = &usage.billing_window {
|
||||||
lines.push(format!("Included {}", formatting::percentage(Some(window), show_used)));
|
let (row_plain, row_markup) = metric_row("Included", Some(window), show_used, None);
|
||||||
|
plain.push(row_plain);
|
||||||
|
markup.push(row_markup);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lines.push(
|
if let Some(label) = &usage.billing_reset_label {
|
||||||
usage
|
let line = format!("Resets {label}");
|
||||||
.billing_reset_label
|
plain.push(format!(" {line}"));
|
||||||
.as_ref()
|
markup.push(format!(" <span foreground=\"#6c7086\">{}</span>", pango_escape(&line)));
|
||||||
.map(|label| format!("Resets {label}"))
|
}
|
||||||
.unwrap_or_else(|| "Reset unknown".to_string()),
|
|
||||||
);
|
|
||||||
for metric in usage.display_additional_usage() {
|
for metric in usage.display_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)));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
lines.push(window_line("5h", usage.short_window.as_ref(), show_used, now));
|
for (label, window) in [("5h", usage.short_window.as_ref()), ("Week", usage.weekly_window.as_ref())] {
|
||||||
lines.push(window_line("Week", usage.weekly_window.as_ref(), show_used, now));
|
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 {
|
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))));
|
(plain.join("\n"), markup.join("\n"))
|
||||||
lines.join("\n")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn window_line(label: &str, window: Option<&UsageWindow>, show_used: bool, now: DateTime<Utc>) -> String {
|
fn header_plain(name: &str, stale: bool, fetched: Option<&str>) -> String {
|
||||||
let percent = formatting::percentage(window, show_used);
|
let suffix = header_suffix(stale, fetched);
|
||||||
let reset = window
|
if suffix.is_empty() {
|
||||||
.and_then(|window| window.reset_at)
|
name.to_string()
|
||||||
.map(|date| formatting::reset(Some(date), now, true))
|
} else {
|
||||||
.unwrap_or_else(|| "—".to_string());
|
format!("{name} {suffix}")
|
||||||
let relative = formatting::relative_reset(window.and_then(|window| window.reset_at), now);
|
}
|
||||||
format!("{label} {percent} {reset} {relative}")
|
}
|
||||||
|
|
||||||
|
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 {
|
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_eq!(view.class, "warning");
|
||||||
assert!(view.tooltip.contains("Cursor models"));
|
assert!(view.tooltip.contains("Cursor models"));
|
||||||
assert!(view.tooltip.contains("Resets Oct 30"));
|
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]
|
#[test]
|
||||||
|
|||||||
Reference in new issue
Block a user