From 6ddfc9e9dccd86d1a339146170f55055838a95b2 Mon Sep 17 00:00:00 2001 From: Paulo Carinhena <63272749+paulocarinhena@users.noreply.github.com> Date: Sun, 1 Nov 2026 16:20:35 -0300 Subject: [PATCH] Data do reset semanal no tooltip e tooltip em formato de tabela MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit O reset semanal que caía no mesmo dia aparecia só com o horário, igual ao das 5 horas. Agora os limites longos sempre dizem o dia ("today · 18:00", "tomorrow · 11:59", "qua 04 nov · 14:28") e o limite de horas continua só com o horário. No Windows o tooltip virou uma tabelinha: conta com o plano numa etiqueta, e por limite o uso e o "↻ reset" alinhados em colunas; o reset de um limite esgotado fica em vermelho. macOS e Linux usam o mesmo formato de data, com "↻" no lugar de "- resets". Co-Authored-By: Claude Opus 5.5 --- widget-linux/openusage.py | 20 +++++--- widget-mac/OpenUsage.swift | 22 +++++--- widget/usage-widget.ps1 | 102 ++++++++++++++++++++++++++++--------- 3 files changed, 104 insertions(+), 40 deletions(-) diff --git a/widget-linux/openusage.py b/widget-linux/openusage.py index 37368fe..a713e28 100755 --- a/widget-linux/openusage.py +++ b/widget-linux/openusage.py @@ -333,12 +333,18 @@ def _is_today(d): return time.localtime(time.time())[:3] == d[:3] -def format_reset(ms): +def format_reset(ms, with_date=False): + """"20:00" today, else "Mon 02 Nov · 11:59". A weekly or monthly limit always names its day ("today · 18:00"), + so it can't be taken for the 5-hour one.""" if ms is None: return "" d = time.localtime(ms / 1000) t = time.strftime("%H:%M", d) - return t if _is_today(d) else time.strftime("%a %d %b", d) + ", " + t + if _is_today(d): + return f"today · {t}" if with_date else t + if time.localtime(time.time() + 86400)[:3] == d[:3]: + return f"tomorrow · {t}" + return time.strftime("%a %d %b", d).replace(".", "") + f" · {t}" def format_reset_short(ms): @@ -416,12 +422,12 @@ def short_account(u): def tip_text(u, display): """Everything the ring leaves out: every limit with its reset, the plan and any error.""" - lines = [(u.get("account") or u["name"]) + (f" - {u['plan'].upper()}" if u.get("plan") else "")] + lines = [(u.get("account") or u["name"]) + (f" {u['plan'].upper()}" if u.get("plan") else "")] for w in u.get("windows") or []: - reset = format_reset(w.get("resetsAt")) + reset = format_reset(w.get("resetsAt"), "Hour" not in w["label"]) lines.append( f"{w['label']} {round(shown_pct(w, display))}% {'used' if display == 'used' else 'left'}" - + (f" - resets {reset}" if reset else "") + + (f" ↻ {reset}" if reset else "") ) for x in u.get("extras") or []: lines.append(f"{x['label']} {x['value']}") @@ -1103,9 +1109,9 @@ def card(u, display, index=0, intro=False): for i, w in enumerate(u.get("windows") or []): pct = shown_pct(w, display) lv = level(w["usedPct"]) - reset = format_reset(w.get("resetsAt")) + reset = format_reset(w.get("resetsAt"), "Hour" not in w["label"]) row = hbox(8) - row.pack_start(label(span(w["label"]) + (span(f" {reset}", 12, C["muted"]) if reset else ""), ellipsize=True), True, True, 0) + row.pack_start(label(span(w["label"]) + (span(f" ↻ {reset}", 11.5, C["muted"]) if reset else ""), ellipsize=True), True, True, 0) row.pack_end(label(span(f"{round(pct)}%", 13, lv or C["text"], "bold"), xalign=1), False, False, 0) box.pack_start(margins(row, top=9 if i == 0 else 8), False, False, 0) meter = Meter(w["usedPct"], lv or C["muted"], f"{uid(u)}|{w['label']}", delay=120 + 70 * index, intro=intro, display=display) diff --git a/widget-mac/OpenUsage.swift b/widget-mac/OpenUsage.swift index c915644..b2a2af1 100644 --- a/widget-mac/OpenUsage.swift +++ b/widget-mac/OpenUsage.swift @@ -420,14 +420,20 @@ func level(_ pct: Double) -> Color? { pct >= 80 ? C.error : pct >= 50 ? C.warn : // ---- formatting ------------------------------------------------------------------ -func formatReset(_ ms: Double?) -> String { +/** + * "20:00" today, else "Mon 02 Nov · 11:59". A weekly or monthly limit always names its day ("today · 18:00"), + * so it can't be taken for the 5-hour one. + */ +func formatReset(_ ms: Double?, withDate: Bool = false) -> String { guard let ms = ms else { return "" } let d = Date(timeIntervalSince1970: ms / 1000) let time = DateFormatter.localizedString(from: d, dateStyle: .none, timeStyle: .short) - if Calendar.current.isDateInToday(d) { return time } + if Calendar.current.isDateInToday(d) { return withDate ? "today \u{00B7} " + time : time } + if Calendar.current.isDateInTomorrow(d) { return "tomorrow \u{00B7} " + time } let f = DateFormatter() f.setLocalizedDateFormatFromTemplate("EEEddMMM") - return f.string(from: d) + ", " + time + let day = f.string(from: d).replacingOccurrences(of: ",", with: "").replacingOccurrences(of: ".", with: "") + return day + " \u{00B7} " + time } /** Shorter, for the compact rows: "14:00", "Wed 14:00" within the week, else "14 Oct". */ @@ -891,8 +897,8 @@ struct Card: View { let pct = display == "used" ? w.usedPct : 100 - w.usedPct let lv = level(w.usedPct) HStack { - let reset = formatReset(w.resetsAt) - (Text(w.label) + Text(reset.isEmpty ? "" : " \(reset)").font(.system(size: 12)).foregroundColor(C.muted)) + let reset = formatReset(w.resetsAt, withDate: !w.label.contains("Hour")) + (Text(w.label) + Text(reset.isEmpty ? "" : " \u{21BB} \(reset)").font(.system(size: 11.5)).foregroundColor(C.muted)) .lineLimit(1).truncationMode(.tail) Spacer() Text("\(Int(pct.rounded()))%").font(.system(size: 13, weight: .semibold)).foregroundColor(lv ?? C.text) @@ -958,12 +964,12 @@ func shortAccount(_ u: ProviderUsage) -> String { /** Everything the ring leaves out: every limit with its reset, the plan and any error. */ func tipText(_ u: ProviderUsage, _ display: String) -> String { - var lines = [(u.account ?? u.name) + (u.plan.map { " - " + $0.uppercased() } ?? "")] + var lines = [(u.account ?? u.name) + (u.plan.map { " " + $0.uppercased() } ?? "")] for w in u.windows { - let reset = formatReset(w.resetsAt) + let reset = formatReset(w.resetsAt, withDate: !w.label.contains("Hour")) lines.append( "\(w.label) \(Int(shown(w, display).rounded()))% \(display == "used" ? "used" : "left")" - + (reset.isEmpty ? "" : " - resets \(reset)")) + + (reset.isEmpty ? "" : " \u{21BB} \(reset)")) } for x in u.extras { lines.append("\(x.label) \(x.value)") } if let e = u.error { lines.append(e) } diff --git a/widget/usage-widget.ps1 b/widget/usage-widget.ps1 index 2f501ce..12022a6 100644 --- a/widget/usage-widget.ps1 +++ b/widget/usage-widget.ps1 @@ -179,11 +179,15 @@ $win.Topmost = if ($null -ne $state.topmost) { [bool]$state.topmost } else { $tr # ---- formatting --------------------------------------------------------------- function Level([double]$pct) { if ($pct -ge 80) { $C.Error } elseif ($pct -ge 50) { $C.Warn } else { $null } } -function Format-Reset($ms) { +# "20:00" today, else "seg 02 nov · 11:59". A weekly or monthly limit always names its day ("today · 18:00"), +# so it can't be taken for the 5-hour one. +function Format-Reset($ms, [switch]$WithDate) { if (-not $ms) { return '' } $d = [DateTimeOffset]::FromUnixTimeMilliseconds([long]$ms).LocalDateTime - if ($d.Date -eq (Get-Date).Date) { return $d.ToString('t') } - return $d.ToString('ddd, dd MMM') + ', ' + $d.ToString('t') + $dot = " $([char]0x00B7) " + if ($d.Date -eq (Get-Date).Date) { return $(if ($WithDate) { "today$dot" } else { '' }) + $d.ToString('t') } + if ($d.Date -eq (Get-Date).Date.AddDays(1)) { return "tomorrow$dot" + $d.ToString('t') } + return $d.ToString('ddd dd MMM').Replace('.', '') + $dot + $d.ToString('t') } # Shorter, for the compact rows: "14:00", "Wed 14:00" within the week, else "14 Oct". @@ -344,8 +348,8 @@ function Render-Normal { $lv = Level $w.usedPct $label = New-Object System.Windows.Controls.TextBlock $label.Inlines.Add((New-Object System.Windows.Documents.Run $w.label)) - $reset = Format-Reset $w.resetsAt - if ($reset) { $label.Inlines.Add((New-Object System.Windows.Documents.Run " $reset" -Property @{ Foreground = (Brush $C.Muted); FontSize = 12 })) } + $reset = Format-Reset $w.resetsAt -WithDate:($w.label -notmatch 'Hour') + if ($reset) { $label.Inlines.Add((New-Object System.Windows.Documents.Run " $([char]0x21BB) $reset" -Property @{ Foreground = (Brush $C.Muted); FontSize = 11.5 })) } $label.TextTrimming = 'CharacterEllipsis' $valueColor = if ($lv) { $lv } else { $C.Text } $row = New-Row $label (New-Text ("{0}%" -f [math]::Round($pct)) $valueColor 13 'SemiBold') @@ -475,34 +479,82 @@ function New-Ring($u, [double]$size = 42) { return $g } -# Everything the ring leaves out: every limit with its reset, the plan and any error. +# Everything the ring leaves out, as a small table: the account and plan, then per limit what's used and when it +# resets, then any extras and the error. function New-Tip($u) { $tt = New-Object System.Windows.Controls.ToolTip $tt.Background = Brush $C.Card $tt.BorderBrush = Brush $C.Border $tt.Foreground = Brush $C.Text - $tt.Padding = '10,8' + $tt.Padding = '12,10,12,10' $sp = New-Object System.Windows.Controls.StackPanel - $title = if ($u.account) { [string]$u.account } else { [string]$u.name } - if ($u.plan) { $title += " - $(([string]$u.plan).ToUpper())" } - $sp.Children.Add((New-Text $title $C.Text 12 'SemiBold')) | Out-Null - foreach ($w in $u.windows) { - $line = "{0} {1}% {2}" -f $w.label, [math]::Round((Get-Shown $w.usedPct)), $(if ($script:display -eq 'used') { 'used' } else { 'left' }) - $reset = Format-Reset $w.resetsAt - if ($reset) { $line += " - resets $reset" } - $row = New-Text $line $C.Muted 11.5 - $row.Margin = '0,3,0,0' - $sp.Children.Add($row) | Out-Null - } - foreach ($x in $u.extras) { - $row = New-Text "$($x.label) $($x.value)" $C.Muted 11.5 - $row.Margin = '0,3,0,0' - $sp.Children.Add($row) | Out-Null + $sp.MinWidth = 230 + + $head = New-Object System.Windows.Controls.DockPanel + if ($u.plan) { + $pill = New-Object System.Windows.Controls.Border + $pill.CornerRadius = 4 + $pill.Padding = '5,1,5,1' + $pill.Margin = '12,0,0,0' + $pill.VerticalAlignment = 'Center' + $pill.Background = Brush $C.Divider + $pill.Child = New-Text (([string]$u.plan).ToUpper()) $C.Muted 10 'SemiBold' + [System.Windows.Controls.DockPanel]::SetDock($pill, 'Right') + $head.Children.Add($pill) | Out-Null + } + $head.Children.Add((New-Text $(if ($u.account) { [string]$u.account } else { [string]$u.name }) $C.Text 12.5 'SemiBold')) | Out-Null + $sp.Children.Add($head) | Out-Null + + $rows = @(@($u.windows) | Where-Object { $_ }) + @(@($u.extras) | Where-Object { $_ }) + if ($rows.Count) { + $line = New-Object System.Windows.Controls.Border + $line.Height = 1 + $line.Background = Brush $C.Divider + $line.Margin = '0,8,0,4' + $sp.Children.Add($line) | Out-Null + # Label | value | reset, each column lined up across the rows. + $grid = New-Object System.Windows.Controls.Grid + foreach ($w in 'Auto', 'Auto', '*') { + $cd = New-Object System.Windows.Controls.ColumnDefinition + $cd.Width = $w + $grid.ColumnDefinitions.Add($cd) + } + $r = 0 + $put = { + param($el, [int]$col) + $el.Margin = if ($col -eq 0) { '0,4,14,0' } elseif ($col -eq 1) { '0,4,16,0' } else { '0,4,0,0' } + [System.Windows.Controls.Grid]::SetRow($el, $r) + [System.Windows.Controls.Grid]::SetColumn($el, $col) + $grid.Children.Add($el) | Out-Null + } + foreach ($w in @($u.windows)) { + $grid.RowDefinitions.Add((New-Object System.Windows.Controls.RowDefinition)) | Out-Null + & $put (New-Text ([string]$w.label) $C.Muted 11.5) 0 + $lv = Level $w.usedPct + $pct = New-Text ("{0}% {1}" -f [math]::Round((Get-Shown $w.usedPct)), $(if ($script:display -eq 'used') { 'used' } else { 'left' })) $(if ($lv) { $lv } else { $C.Text }) 11.5 'SemiBold' + & $put $pct 1 + $reset = Format-Reset $w.resetsAt -WithDate:($w.label -notmatch 'Hour') + if ($reset) { + $rt = New-Text "$([char]0x21BB) $reset" $(if (Test-Out $w) { $C.Error } else { $C.Muted }) 11.5 + $rt.HorizontalAlignment = 'Right' + & $put $rt 2 + } + $r++ + } + foreach ($x in @($u.extras)) { + $grid.RowDefinitions.Add((New-Object System.Windows.Controls.RowDefinition)) | Out-Null + & $put (New-Text ([string]$x.label) $C.Muted 11.5) 0 + & $put (New-Text ([string]$x.value) $C.Text 11.5 'SemiBold') 1 + $r++ + } + $sp.Children.Add($grid) | Out-Null } if ($u.error) { - $row = New-Text ([string]$u.error) $C.Warn 11.5 - $row.Margin = '0,5,0,0' - $sp.Children.Add($row) | Out-Null + $e = New-Text ([string]$u.error) $C.Warn 11.5 + $e.TextWrapping = 'Wrap' + $e.MaxWidth = 280 + $e.Margin = '0,8,0,0' + $sp.Children.Add($e) | Out-Null } $tt.Content = $sp return $tt