diff --git a/widget-linux/openusage.py b/widget-linux/openusage.py index a96e696..37368fe 100755 --- a/widget-linux/openusage.py +++ b/widget-linux/openusage.py @@ -1169,6 +1169,17 @@ def short_slot(w): return "cr" if n.startswith("Credit") else "day" if n.startswith("Daily") else n[:3].lower() if len(n) > 3 else n +def is_out(w): + """Used up and still waiting on its reset (a reset already past means the numbers are behind, not the limit).""" + return w is not None and w["usedPct"] >= 100 and w.get("resetsAt") is not None and w["resetsAt"] > time.time() * 1000 + + +def blocked_slot(short, long): + """The used-up limit that frees up last (when you can work again), or None while every limit has room.""" + out = [w for w in (short, long) if is_out(w)] + return max(out, key=lambda w: w["resetsAt"]) if out else None + + def meter_line(u, w, display, accent, length, index, intro): """5h ▬▬▬▬▭▭ 72%""" lv = level(w["usedPct"]) @@ -1176,6 +1187,12 @@ def meter_line(u, w, display, accent, length, index, intro): name = label(span(short_slot(w), 10, C["muted"])) name.set_size_request(16, -1) line.pack_start(name, False, False, 0) + # Used up: no bar, just when it comes back, in the room the bar and the number take. + if is_out(w): + back = label(span(f"↻ {format_reset_short(w['resetsAt'])}", 11, C["error"], "bold")) + back.set_size_request(length + 32, -1) + line.pack_start(margins(back, left=3), False, False, 0) + return line meter = Meter(w["usedPct"], lv or accent, f"{uid(u)}|{w['label']}|m", length, 5, 120 + 70 * index, intro, display) line.pack_start(margins(meter, left=3, right=5), False, False, 0) pct = label(span(f"{round(shown_pct(w, display))}%", 11, lv or C["text"], "bold")) @@ -1281,6 +1298,12 @@ def strip_chip(u, display, gauge, index, intro): sub, color = f"{slot_name(other)} {round(shown_pct(other, display))}%", level(other["usedPct"]) or C["muted"] else: sub, color = short_account(u), C["muted"] + # A used-up limit: the second line tells when it comes back instead. + blocked = blocked_slot(short, long) + if blocked is not None: + # Named when it isn't the limit on the line above. + name = "" if blocked is main else f"{slot_name(blocked)} " + sub, color = f"{name}↻ {format_reset_short(blocked['resetsAt'])}", C["error"] elif extras: col.pack_start(label(span(extras[0]["value"], 13, weight="bold")), False, False, 0) sub, color = extras[0]["label"].lower(), C["muted"] diff --git a/widget-mac/OpenUsage.swift b/widget-mac/OpenUsage.swift index 66625b2..c915644 100644 --- a/widget-mac/OpenUsage.swift +++ b/widget-mac/OpenUsage.swift @@ -775,6 +775,11 @@ func shortSlot(_ w: UsageWindow) -> String { return n.count > 3 ? String(n.prefix(3)).lowercased() : n } +/** Used up and still waiting on its reset (a reset already past means the numbers are behind, not the limit). */ +func isOut(_ w: UsageWindow) -> Bool { + w.usedPct >= 100 && (w.resetsAt ?? 0) > Date().timeIntervalSince1970 * 1000 +} + /** 5h ▬▬▬▬▭▭ 72% */ struct MeterLine: View { let w: UsageWindow @@ -787,11 +792,18 @@ struct MeterLine: View { let lv = level(w.usedPct) HStack(spacing: 0) { Text(shortSlot(w)).font(.system(size: 10)).foregroundColor(C.muted).frame(minWidth: 16, alignment: .leading) - Meter(used: w.usedPct, display: display, color: lv ?? accent, width: length, height: 5, delay: 0.12 + 0.07 * Double(index)) - .padding(.leading, 3) - .padding(.trailing, 5) - Text("\(Int(shown(w, display).rounded()))%").font(.system(size: 11, weight: .semibold)).foregroundColor(lv ?? C.text) - .frame(minWidth: 27, alignment: .leading) + if isOut(w) { + // Used up: no bar, just when it comes back, in the room the bar and the number take. + Text("\u{21BB} \(formatResetShort(w.resetsAt))").font(.system(size: 11, weight: .semibold)).foregroundColor(C.error) + .frame(minWidth: length + 32, alignment: .leading) + .padding(.leading, 3) + } else { + Meter(used: w.usedPct, display: display, color: lv ?? accent, width: length, height: 5, delay: 0.12 + 0.07 * Double(index)) + .padding(.leading, 3) + .padding(.trailing, 5) + Text("\(Int(shown(w, display).rounded()))%").font(.system(size: 11, weight: .semibold)).foregroundColor(lv ?? C.text) + .frame(minWidth: 27, alignment: .leading) + } } } } @@ -1157,6 +1169,11 @@ struct CompactCard: View { // ---- strip --------------------------------------------------------------------------- +/** The used-up limit that frees up last (when you can work again), or nil while every limit has room. */ +func blocked(_ s: Slots) -> UsageWindow? { + [s.short, s.long].compactMap { $0 }.filter(isOut).max { $0.resetsAt! < $1.resetsAt! } +} + /** One account in the strip: a ring pair around the mark (or the mark and a meter per limit) and its numbers. */ struct StripChip: View { let u: ProviderUsage @@ -1225,7 +1242,11 @@ struct StripChip: View { (Text("\(Int(shown(main, display).rounded()))%").font(.system(size: 13, weight: .semibold)) .foregroundColor(level(main.usedPct) ?? C.text) + Text(" \(slotName(main))").font(.system(size: 10)).foregroundColor(C.muted)) - if let other = other { + // A used-up limit: the second line tells when it comes back instead. + if let b = blocked(s) { + // Named when it isn't the limit on the line above. + Text("\(b.label == main.label ? "" : slotName(b) + " ")\u{21BB} \(formatResetShort(b.resetsAt))").font(.system(size: 10)).foregroundColor(C.error) + } else if let other = other { Text("\(slotName(other)) \(Int(shown(other, display).rounded()))%").font(.system(size: 10)) .foregroundColor(level(other.usedPct) ?? C.muted) } else if !shortAccount(u).isEmpty { diff --git a/widget/usage-widget.ps1 b/widget/usage-widget.ps1 index cacfe2f..06cedd5 100644 --- a/widget/usage-widget.ps1 +++ b/widget/usage-widget.ps1 @@ -387,6 +387,14 @@ function Get-Shown([double]$used) { if ($script:display -eq 'used') { $used } el # How much of a ring to fill: the shown value, except that a used-up limit is always a full ring (in red). function Get-ArcPct([double]$used) { if ($used -ge 100) { 100 } else { Get-Shown $used } } +# Used up and still waiting on its reset (a reset already past means the numbers are behind, not the limit). +function Test-Out($w) { $w -and $w.usedPct -ge 100 -and $w.resetsAt -and [double]$w.resetsAt -gt [DateTimeOffset]::Now.ToUnixTimeMilliseconds() } + +# The used-up limit that frees up last (when you can work again), or nothing while every limit has room. +function Get-Blocked($slots) { + @($slots.short, $slots.long) | Where-Object { Test-Out $_ } | Sort-Object { [double]$_.resetsAt } | Select-Object -Last 1 +} + function Get-SlotName($w) { if (-not $w) { return '' } switch -Regex ($w.label) { 'Hour' { '5h' } 'Week|7-Day' { 'wk' } 'Month|Billing' { 'mo' } default { $w.label } } @@ -849,6 +857,15 @@ function New-Meter($w, [string]$accent, [int]$index, [double]$len = 46) { $name.MinWidth = 16 $name.VerticalAlignment = 'Center' $row.Children.Add($name) | Out-Null + # Used up: no bar, just when it comes back, in the room the bar and the number take. + if (Test-Out $w) { + $back = New-Text "$([char]0x21BB) $(Format-ResetShort $w.resetsAt)" $C.Error 11 'SemiBold' + $back.MinWidth = $len + 32 + $back.Margin = '3,0,0,0' + $back.VerticalAlignment = 'Center' + $row.Children.Add($back) | Out-Null + return $row + } $track = New-Fill $used $color $index 5 $track.Width = $len $track.VerticalAlignment = 'Center' @@ -942,8 +959,15 @@ function New-BarChip($u, [int]$index) { $text.Children.Add((New-Text ([string]($u.name -replace ' \(.*\)$', '')) $C.Text 12 'SemiBold')) | Out-Null $sub = 'no data' } + $lv = if ($other) { Level $other.usedPct } else { $null } + # A used-up limit: the second line tells when it comes back instead. + $blocked = Get-Blocked $slots + if ($blocked) { + # Named when it isn't the limit on the line above. + $sub = "$(if ($blocked -ne $main) { (Get-SlotName $blocked) + ' ' })$([char]0x21BB) $(Format-ResetShort $blocked.resetsAt)" + $lv = $C.Error + } if ($sub) { - $lv = if ($other) { Level $other.usedPct } else { $null } $s2 = New-Text $sub $(if ($lv) { $lv } else { $C.Muted }) 10 $s2.Margin = '0,-1,0,0' $text.Children.Add($s2) | Out-Null