usage-cli 0.29.32__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- adapters/__init__.py +5 -0
- adapters/agy.py +68 -0
- adapters/claude.py +215 -0
- adapters/codex.py +209 -0
- adapters/rate_limits.py +76 -0
- adapters/registry.py +17 -0
- adapters/types.py +139 -0
- agy_disk_cache.py +135 -0
- agy_loader.py +416 -0
- agy_quota_probe.py +748 -0
- agy_window_keeper.py +185 -0
- analyzer/__init__.py +5 -0
- analyzer/aggregator.py +139 -0
- analyzer/blocks.py +80 -0
- analyzer/diagnoser.py +638 -0
- analyzer/insights.py +277 -0
- analyzer/persona_loader.py +199 -0
- analyzer/reporter.py +989 -0
- analyzer/subscription.py +108 -0
- burn_rate.py +75 -0
- cache_quarantine.py +50 -0
- codex_disk_cache.py +227 -0
- codex_events.py +136 -0
- codex_fork_replay.py +111 -0
- codex_loader.py +1426 -0
- codex_paths.py +20 -0
- critter_frames.py +26 -0
- discussion_bridge.py +1196 -0
- discussion_cli.py +844 -0
- discussion_session.py +622 -0
- discussion_usage.py +13 -0
- discussion_window.py +955 -0
- disk_cache_common.py +132 -0
- disk_cache_lifecycle.py +39 -0
- doctor.py +452 -0
- fsevents_watch.py +207 -0
- history_disk_cache.py +110 -0
- history_loader.py +416 -0
- i18n.py +88 -0
- jsonl_limits.py +17 -0
- jsonl_utils.py +40 -0
- login_item.py +154 -0
- main.py +387 -0
- menubar.py +1201 -0
- menubar_actions.py +204 -0
- menubar_agy.py +193 -0
- menubar_chrome.py +156 -0
- menubar_menu.py +169 -0
- menubar_notify.py +102 -0
- menubar_popover.py +233 -0
- menubar_prefs.py +118 -0
- menubar_refresh.py +285 -0
- menubar_state.py +1200 -0
- menubar_title.py +157 -0
- menubar_update.py +123 -0
- panel_window.py +78 -0
- panel_window_state.py +159 -0
- panels/__init__.py +186 -0
- panels/base.py +83 -0
- panels/dynamic_height.py +140 -0
- panels/payload.py +178 -0
- panels/web_panel.py +513 -0
- panels/window_drag.py +56 -0
- prefs.py +44 -0
- pricing.py +452 -0
- project_resolver.py +112 -0
- service_status.py +383 -0
- session_hooks.py +1154 -0
- setup_app.py +171 -0
- setup_hook.py +1011 -0
- statusline_settings.py +160 -0
- talent_market_bridge.py +243 -0
- time_utils.py +24 -0
- tui.py +288 -0
- tui_sprite.py +206 -0
- ui/__init__.py +5 -0
- ui/html_report.py +923 -0
- ui/report_scripts.py +251 -0
- ui/report_styles.py +370 -0
- ui/tables.py +888 -0
- update_checker.py +156 -0
- update_gate.py +66 -0
- update_release_notes.py +49 -0
- usage_cli-0.29.32.data/data/share/usage/i18n.json +2427 -0
- usage_cli-0.29.32.dist-info/METADATA +223 -0
- usage_cli-0.29.32.dist-info/RECORD +109 -0
- usage_cli-0.29.32.dist-info/WHEEL +5 -0
- usage_cli-0.29.32.dist-info/entry_points.txt +3 -0
- usage_cli-0.29.32.dist-info/licenses/LICENSE +663 -0
- usage_cli-0.29.32.dist-info/top_level.txt +80 -0
- usage_cli.py +827 -0
- usage_client.py +487 -0
- usage_diagnosis_snapshot.py +143 -0
- usage_dir_sweeper.py +100 -0
- usage_lang.py +79 -0
- usage_logging.py +75 -0
- usage_notifications.py +96 -0
- usage_rate.py +97 -0
- usage_session_resume.py +913 -0
- usage_statusline.py +810 -0
- usage_statusline_agy.py +397 -0
- usage_statusline_forwarder.py +88 -0
- usage_terse_mode.py +223 -0
- usage_terse_reminder.py +151 -0
- win_login_item.py +53 -0
- window_keeper.py +264 -0
- windows_watch.py +443 -0
- wintray.py +2014 -0
- wintray_menu.py +136 -0
tui.py
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
# Copyright (C) 2026 lollapalooza <https://github.com/aqua5230>
|
|
3
|
+
#
|
|
4
|
+
# Part of "usage". Free software licensed under the GNU Affero General Public
|
|
5
|
+
# License v3.0 only; see the LICENSE file for full terms and the warranty disclaimer.
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import math
|
|
11
|
+
import time
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from functools import lru_cache
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from rich.align import Align
|
|
17
|
+
from rich.console import Console, Group, RenderableType
|
|
18
|
+
from rich.panel import Panel
|
|
19
|
+
from rich.table import Table
|
|
20
|
+
from rich.text import Text
|
|
21
|
+
|
|
22
|
+
from i18n import packaged_resource_path
|
|
23
|
+
from tui_sprite import render_sprite
|
|
24
|
+
from usage_client import PollState, UsageSnapshot
|
|
25
|
+
from usage_lang import detect_lang
|
|
26
|
+
|
|
27
|
+
BG = "#000000"
|
|
28
|
+
PANEL = "#1f1f1e"
|
|
29
|
+
TEXT = "#faf9f5"
|
|
30
|
+
DIM = "#b0aea5"
|
|
31
|
+
ACCENT = "#d97757"
|
|
32
|
+
GREEN = "#788c5d"
|
|
33
|
+
RED = "#c0392b"
|
|
34
|
+
BAR_BG = "#2a2a28"
|
|
35
|
+
|
|
36
|
+
SPINNER_FRAMES = ["·", "✻", "✽", "✶", "✳", "✢"]
|
|
37
|
+
SPINNER_PHASES = [0, 1, 2, 3, 4, 5, 4, 3, 2, 1]
|
|
38
|
+
SPINNER_PHASE_MS = [260, 130, 130, 130, 130, 260, 130, 130, 130, 130]
|
|
39
|
+
LOADING_INTERVAL_MS = 4000
|
|
40
|
+
I18N_PATH = packaged_resource_path("i18n.json", Path(__file__).with_name("i18n.json"))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@lru_cache(maxsize=1)
|
|
44
|
+
def _load_i18n_bundle() -> dict[str, dict[str, str]]:
|
|
45
|
+
data = json.loads(I18N_PATH.read_text(encoding="utf-8"))
|
|
46
|
+
return {
|
|
47
|
+
str(lang): {str(key): str(value) for key, value in values.items()}
|
|
48
|
+
for lang, values in data.items()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _t(language: str, key: str, **kwargs: object) -> str:
|
|
53
|
+
bundle = _load_i18n_bundle()
|
|
54
|
+
table = bundle.get(language) or bundle["en"]
|
|
55
|
+
template = table.get(key) or bundle["en"].get(key) or key
|
|
56
|
+
return template.format(**kwargs)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _loading_phrases(language: str) -> list[str]:
|
|
60
|
+
return _t(language, "loading_phrases").split("|")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(slots=True)
|
|
64
|
+
class AppViewState:
|
|
65
|
+
language: str = field(default_factory=detect_lang)
|
|
66
|
+
poll_state: PollState = PollState.LOADING
|
|
67
|
+
snapshot: UsageSnapshot | None = None
|
|
68
|
+
message: str = ""
|
|
69
|
+
fatal_message: str | None = None
|
|
70
|
+
started_at: float = field(default_factory=time.monotonic)
|
|
71
|
+
rate_group: int = 0
|
|
72
|
+
codex_credits: tuple[str | None, bool] | None = None
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def format_countdown(reset_at: float, language: str, now: float | None = None) -> str:
|
|
76
|
+
current_time = now if now is not None else time.time()
|
|
77
|
+
remaining = max(0, math.ceil(reset_at - current_time))
|
|
78
|
+
days, rem = divmod(remaining, 86400)
|
|
79
|
+
hours, rem = divmod(rem, 3600)
|
|
80
|
+
minutes, seconds = divmod(rem, 60)
|
|
81
|
+
|
|
82
|
+
if days > 0:
|
|
83
|
+
return _t(language, "resets_in_days", days=days, hours=hours)
|
|
84
|
+
if hours > 0:
|
|
85
|
+
return _t(language, "resets_in_hours", hours=hours, minutes=minutes)
|
|
86
|
+
return _t(language, "resets_in_minutes", minutes=minutes, seconds=seconds)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _bar_style(percent: int) -> str:
|
|
90
|
+
if percent < 50:
|
|
91
|
+
return GREEN
|
|
92
|
+
if percent <= 80:
|
|
93
|
+
return ACCENT
|
|
94
|
+
return RED
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _build_progress_line(percent: int, width: int = 28) -> Text:
|
|
98
|
+
filled = round((percent / 100) * width)
|
|
99
|
+
text = Text()
|
|
100
|
+
text.append("▄" * filled, style=f"bold {_bar_style(percent)}")
|
|
101
|
+
text.append("▄" * max(0, width - filled), style=f"bold {BAR_BG}")
|
|
102
|
+
return text
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _chip(label: str) -> Text:
|
|
106
|
+
return Text(f" {label} ", style=f"bold {TEXT} on {PANEL}")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _usage_block(
|
|
110
|
+
percent: int,
|
|
111
|
+
label: str,
|
|
112
|
+
reset_at: float,
|
|
113
|
+
now: float,
|
|
114
|
+
language: str,
|
|
115
|
+
) -> RenderableType:
|
|
116
|
+
row = Table.grid(expand=False, padding=(0, 1))
|
|
117
|
+
row.add_column(width=4)
|
|
118
|
+
row.add_column(width=16)
|
|
119
|
+
row.add_column(width=11)
|
|
120
|
+
row.add_row(
|
|
121
|
+
Text(f"{percent}%", style=f"bold {_bar_style(percent)}"),
|
|
122
|
+
_build_progress_line(percent, width=15),
|
|
123
|
+
_chip(label),
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
countdown = Text.assemble(" ", (format_countdown(reset_at, language, now), DIM))
|
|
127
|
+
return Group(row, countdown)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _missing_usage_block(label: str, language: str) -> RenderableType:
|
|
131
|
+
row = Table.grid(expand=False, padding=(0, 1))
|
|
132
|
+
row.add_column(width=4)
|
|
133
|
+
row.add_column(width=16)
|
|
134
|
+
row.add_column(width=11)
|
|
135
|
+
row.add_row(
|
|
136
|
+
Text("--", style=f"bold {DIM}"),
|
|
137
|
+
_build_progress_line(0, width=15),
|
|
138
|
+
_chip(label),
|
|
139
|
+
)
|
|
140
|
+
return Group(row, Text(f" {_t(language, 'resets_in_placeholder')}", style=DIM))
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _spinner_frame(now: float, started_at: float) -> str:
|
|
144
|
+
elapsed_ms = int((now - started_at) * 1000)
|
|
145
|
+
total_ms = sum(SPINNER_PHASE_MS)
|
|
146
|
+
phase_ms = elapsed_ms % total_ms
|
|
147
|
+
accumulated = 0
|
|
148
|
+
|
|
149
|
+
for phase_index, duration in enumerate(SPINNER_PHASE_MS):
|
|
150
|
+
accumulated += duration
|
|
151
|
+
if phase_ms < accumulated:
|
|
152
|
+
return SPINNER_FRAMES[SPINNER_PHASES[phase_index]]
|
|
153
|
+
return SPINNER_FRAMES[SPINNER_PHASES[-1]]
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _status_line(state: AppViewState, now: float) -> Text:
|
|
157
|
+
spinner = _spinner_frame(now, state.started_at)
|
|
158
|
+
elapsed_ms = (now - state.started_at) * 1000
|
|
159
|
+
phrases = _loading_phrases(state.language)
|
|
160
|
+
phrase_index = int(elapsed_ms / LOADING_INTERVAL_MS) % len(phrases)
|
|
161
|
+
|
|
162
|
+
color = ACCENT
|
|
163
|
+
phrase = phrases[phrase_index]
|
|
164
|
+
|
|
165
|
+
if state.poll_state == PollState.RATE_LIMITED:
|
|
166
|
+
phrase = _t(state.language, "status_rate_limited")
|
|
167
|
+
color = RED
|
|
168
|
+
elif state.poll_state == PollState.TOKEN_ERROR:
|
|
169
|
+
phrase = _t(state.language, "status_token_unavailable")
|
|
170
|
+
color = ACCENT
|
|
171
|
+
elif state.poll_state in (PollState.CONNECTION_ERROR, PollState.FATAL):
|
|
172
|
+
phrase = _t(state.language, "status_api_offline")
|
|
173
|
+
color = RED
|
|
174
|
+
|
|
175
|
+
return Text.assemble(
|
|
176
|
+
(f"{spinner} ", f"bold {color}"),
|
|
177
|
+
(phrase, f"bold {color}"),
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def render_screen(state: AppViewState, frame_index: int) -> Panel:
|
|
182
|
+
now = time.time()
|
|
183
|
+
anim_now = time.monotonic()
|
|
184
|
+
panel_width = min(Console().size.width, 60)
|
|
185
|
+
|
|
186
|
+
# Group label and colors
|
|
187
|
+
groups = [
|
|
188
|
+
_t(state.language, "group_idle"),
|
|
189
|
+
_t(state.language, "group_normal"),
|
|
190
|
+
_t(state.language, "group_active"),
|
|
191
|
+
_t(state.language, "group_heavy"),
|
|
192
|
+
]
|
|
193
|
+
group_colors = [DIM, TEXT, ACCENT, RED]
|
|
194
|
+
group_label = groups[state.rate_group]
|
|
195
|
+
group_color = group_colors[state.rate_group]
|
|
196
|
+
|
|
197
|
+
# Status dot color
|
|
198
|
+
dot_color = ACCENT
|
|
199
|
+
if state.poll_state == PollState.SUCCESS:
|
|
200
|
+
dot_color = GREEN
|
|
201
|
+
elif state.poll_state in (
|
|
202
|
+
PollState.TOKEN_ERROR,
|
|
203
|
+
PollState.CONNECTION_ERROR,
|
|
204
|
+
PollState.RATE_LIMITED,
|
|
205
|
+
PollState.FATAL,
|
|
206
|
+
):
|
|
207
|
+
dot_color = RED
|
|
208
|
+
|
|
209
|
+
title_table = Table.grid(expand=True, padding=(0, 0))
|
|
210
|
+
title_table.add_column(width=10)
|
|
211
|
+
title_table.add_column(ratio=1, justify="left")
|
|
212
|
+
title_table.add_column(width=4, justify="right")
|
|
213
|
+
|
|
214
|
+
title_left = Text.assemble(
|
|
215
|
+
(f"[{group_label}]", f"bold {group_color}"),
|
|
216
|
+
(" •", f"bold {dot_color}"),
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
title_table.add_row(
|
|
220
|
+
render_sprite(frame_index),
|
|
221
|
+
Text.assemble((f" {_t(state.language, 'usage_title')} ", f"bold {TEXT}"), title_left),
|
|
222
|
+
Text("🔋"),
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
body_items: list[RenderableType] = []
|
|
226
|
+
|
|
227
|
+
if state.snapshot is None:
|
|
228
|
+
loading_block = Group(
|
|
229
|
+
_usage_block(0, _t(state.language, "current_label"), now + 3600, now, state.language),
|
|
230
|
+
Text(""),
|
|
231
|
+
_usage_block(0, _t(state.language, "weekly_label"), now + 86400, now, state.language),
|
|
232
|
+
)
|
|
233
|
+
body_items.append(loading_block)
|
|
234
|
+
else:
|
|
235
|
+
current_block = (
|
|
236
|
+
_usage_block(
|
|
237
|
+
state.snapshot.current_percent,
|
|
238
|
+
_t(state.language, "current_label"),
|
|
239
|
+
state.snapshot.current_reset_at,
|
|
240
|
+
now,
|
|
241
|
+
state.language,
|
|
242
|
+
)
|
|
243
|
+
if state.snapshot.current_percent is not None
|
|
244
|
+
else _missing_usage_block(_t(state.language, "current_label"), state.language)
|
|
245
|
+
)
|
|
246
|
+
weekly_block = (
|
|
247
|
+
_usage_block(
|
|
248
|
+
state.snapshot.weekly_percent,
|
|
249
|
+
_t(state.language, "weekly_label"),
|
|
250
|
+
state.snapshot.weekly_reset_at,
|
|
251
|
+
now,
|
|
252
|
+
state.language,
|
|
253
|
+
)
|
|
254
|
+
if state.snapshot.weekly_percent is not None
|
|
255
|
+
else _missing_usage_block(_t(state.language, "weekly_label"), state.language)
|
|
256
|
+
)
|
|
257
|
+
body_items.append(
|
|
258
|
+
Group(
|
|
259
|
+
current_block,
|
|
260
|
+
Text(""),
|
|
261
|
+
weekly_block,
|
|
262
|
+
)
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
if state.codex_credits is not None:
|
|
266
|
+
balance, unlimited = state.codex_credits
|
|
267
|
+
text = (
|
|
268
|
+
_t(state.language, "codex_credits_unlimited")
|
|
269
|
+
if unlimited
|
|
270
|
+
else _t(state.language, "codex_credits", balance=balance or "--")
|
|
271
|
+
)
|
|
272
|
+
body_items.append(Text(text, style=DIM))
|
|
273
|
+
|
|
274
|
+
screen = Group(
|
|
275
|
+
title_table,
|
|
276
|
+
Text(""),
|
|
277
|
+
*body_items,
|
|
278
|
+
Text(""),
|
|
279
|
+
Align.center(_status_line(state, anim_now)),
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
return Panel(
|
|
283
|
+
screen,
|
|
284
|
+
border_style=PANEL,
|
|
285
|
+
padding=(1, 2),
|
|
286
|
+
width=panel_width,
|
|
287
|
+
style=f"on {BG}",
|
|
288
|
+
)
|
tui_sprite.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
# Copyright (C) 2026 lollapalooza <https://github.com/aqua5230>
|
|
3
|
+
#
|
|
4
|
+
# Part of "usage". Free software licensed under the GNU Affero General Public
|
|
5
|
+
# License v3.0 only; see the LICENSE file for full terms and the warranty disclaimer.
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from rich.style import Style
|
|
10
|
+
from rich.text import Text
|
|
11
|
+
|
|
12
|
+
ORANGE = "#d97757"
|
|
13
|
+
EYE = "#1A1A1A"
|
|
14
|
+
|
|
15
|
+
Pixel = str | None
|
|
16
|
+
Frame = list[list[Pixel]]
|
|
17
|
+
|
|
18
|
+
TRANSPARENT: Pixel = None
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
FRAMES: list[Frame] = [
|
|
22
|
+
[
|
|
23
|
+
[
|
|
24
|
+
TRANSPARENT,
|
|
25
|
+
TRANSPARENT,
|
|
26
|
+
ORANGE,
|
|
27
|
+
ORANGE,
|
|
28
|
+
ORANGE,
|
|
29
|
+
ORANGE,
|
|
30
|
+
ORANGE,
|
|
31
|
+
TRANSPARENT,
|
|
32
|
+
TRANSPARENT,
|
|
33
|
+
],
|
|
34
|
+
[TRANSPARENT, ORANGE, ORANGE, ORANGE, ORANGE, ORANGE, ORANGE, ORANGE, TRANSPARENT],
|
|
35
|
+
[ORANGE, ORANGE, EYE, ORANGE, ORANGE, ORANGE, EYE, ORANGE, ORANGE],
|
|
36
|
+
[ORANGE, ORANGE, EYE, ORANGE, ORANGE, ORANGE, EYE, ORANGE, ORANGE],
|
|
37
|
+
[ORANGE, ORANGE, ORANGE, ORANGE, ORANGE, ORANGE, ORANGE, ORANGE, ORANGE],
|
|
38
|
+
[EYE, ORANGE, ORANGE, ORANGE, ORANGE, ORANGE, ORANGE, ORANGE, ORANGE],
|
|
39
|
+
[
|
|
40
|
+
TRANSPARENT,
|
|
41
|
+
ORANGE,
|
|
42
|
+
TRANSPARENT,
|
|
43
|
+
ORANGE,
|
|
44
|
+
TRANSPARENT,
|
|
45
|
+
ORANGE,
|
|
46
|
+
TRANSPARENT,
|
|
47
|
+
ORANGE,
|
|
48
|
+
TRANSPARENT,
|
|
49
|
+
],
|
|
50
|
+
[
|
|
51
|
+
TRANSPARENT,
|
|
52
|
+
ORANGE,
|
|
53
|
+
TRANSPARENT,
|
|
54
|
+
ORANGE,
|
|
55
|
+
TRANSPARENT,
|
|
56
|
+
ORANGE,
|
|
57
|
+
TRANSPARENT,
|
|
58
|
+
ORANGE,
|
|
59
|
+
TRANSPARENT,
|
|
60
|
+
],
|
|
61
|
+
],
|
|
62
|
+
[
|
|
63
|
+
[
|
|
64
|
+
TRANSPARENT,
|
|
65
|
+
TRANSPARENT,
|
|
66
|
+
ORANGE,
|
|
67
|
+
ORANGE,
|
|
68
|
+
ORANGE,
|
|
69
|
+
ORANGE,
|
|
70
|
+
ORANGE,
|
|
71
|
+
TRANSPARENT,
|
|
72
|
+
TRANSPARENT,
|
|
73
|
+
],
|
|
74
|
+
[TRANSPARENT, ORANGE, ORANGE, ORANGE, ORANGE, ORANGE, ORANGE, ORANGE, TRANSPARENT],
|
|
75
|
+
[ORANGE, ORANGE, ORANGE, ORANGE, ORANGE, ORANGE, ORANGE, ORANGE, ORANGE],
|
|
76
|
+
[ORANGE, ORANGE, EYE, ORANGE, ORANGE, ORANGE, EYE, ORANGE, ORANGE],
|
|
77
|
+
[ORANGE, ORANGE, ORANGE, ORANGE, ORANGE, ORANGE, ORANGE, ORANGE, ORANGE],
|
|
78
|
+
[EYE, ORANGE, ORANGE, ORANGE, ORANGE, ORANGE, ORANGE, ORANGE, ORANGE],
|
|
79
|
+
[
|
|
80
|
+
TRANSPARENT,
|
|
81
|
+
ORANGE,
|
|
82
|
+
TRANSPARENT,
|
|
83
|
+
ORANGE,
|
|
84
|
+
TRANSPARENT,
|
|
85
|
+
ORANGE,
|
|
86
|
+
TRANSPARENT,
|
|
87
|
+
ORANGE,
|
|
88
|
+
TRANSPARENT,
|
|
89
|
+
],
|
|
90
|
+
[
|
|
91
|
+
TRANSPARENT,
|
|
92
|
+
ORANGE,
|
|
93
|
+
TRANSPARENT,
|
|
94
|
+
ORANGE,
|
|
95
|
+
TRANSPARENT,
|
|
96
|
+
ORANGE,
|
|
97
|
+
TRANSPARENT,
|
|
98
|
+
ORANGE,
|
|
99
|
+
TRANSPARENT,
|
|
100
|
+
],
|
|
101
|
+
],
|
|
102
|
+
[
|
|
103
|
+
[
|
|
104
|
+
TRANSPARENT,
|
|
105
|
+
TRANSPARENT,
|
|
106
|
+
ORANGE,
|
|
107
|
+
ORANGE,
|
|
108
|
+
ORANGE,
|
|
109
|
+
ORANGE,
|
|
110
|
+
ORANGE,
|
|
111
|
+
TRANSPARENT,
|
|
112
|
+
TRANSPARENT,
|
|
113
|
+
],
|
|
114
|
+
[TRANSPARENT, ORANGE, ORANGE, ORANGE, ORANGE, ORANGE, ORANGE, ORANGE, TRANSPARENT],
|
|
115
|
+
[ORANGE, ORANGE, EYE, ORANGE, ORANGE, ORANGE, EYE, ORANGE, ORANGE],
|
|
116
|
+
[ORANGE, ORANGE, EYE, ORANGE, ORANGE, ORANGE, EYE, ORANGE, ORANGE],
|
|
117
|
+
[ORANGE, ORANGE, ORANGE, ORANGE, ORANGE, ORANGE, ORANGE, ORANGE, ORANGE],
|
|
118
|
+
[EYE, ORANGE, ORANGE, ORANGE, ORANGE, ORANGE, ORANGE, ORANGE, ORANGE],
|
|
119
|
+
[
|
|
120
|
+
TRANSPARENT,
|
|
121
|
+
TRANSPARENT,
|
|
122
|
+
TRANSPARENT,
|
|
123
|
+
ORANGE,
|
|
124
|
+
TRANSPARENT,
|
|
125
|
+
ORANGE,
|
|
126
|
+
TRANSPARENT,
|
|
127
|
+
ORANGE,
|
|
128
|
+
TRANSPARENT,
|
|
129
|
+
],
|
|
130
|
+
[
|
|
131
|
+
TRANSPARENT,
|
|
132
|
+
ORANGE,
|
|
133
|
+
TRANSPARENT,
|
|
134
|
+
ORANGE,
|
|
135
|
+
TRANSPARENT,
|
|
136
|
+
ORANGE,
|
|
137
|
+
TRANSPARENT,
|
|
138
|
+
ORANGE,
|
|
139
|
+
TRANSPARENT,
|
|
140
|
+
],
|
|
141
|
+
],
|
|
142
|
+
[
|
|
143
|
+
[
|
|
144
|
+
TRANSPARENT,
|
|
145
|
+
TRANSPARENT,
|
|
146
|
+
ORANGE,
|
|
147
|
+
ORANGE,
|
|
148
|
+
ORANGE,
|
|
149
|
+
ORANGE,
|
|
150
|
+
ORANGE,
|
|
151
|
+
TRANSPARENT,
|
|
152
|
+
TRANSPARENT,
|
|
153
|
+
],
|
|
154
|
+
[TRANSPARENT, ORANGE, ORANGE, ORANGE, ORANGE, ORANGE, ORANGE, ORANGE, TRANSPARENT],
|
|
155
|
+
[ORANGE, ORANGE, EYE, ORANGE, ORANGE, ORANGE, EYE, ORANGE, ORANGE],
|
|
156
|
+
[ORANGE, ORANGE, EYE, ORANGE, ORANGE, ORANGE, EYE, ORANGE, ORANGE],
|
|
157
|
+
[ORANGE, ORANGE, ORANGE, ORANGE, ORANGE, ORANGE, ORANGE, ORANGE, ORANGE],
|
|
158
|
+
[EYE, ORANGE, ORANGE, ORANGE, ORANGE, ORANGE, ORANGE, ORANGE, ORANGE],
|
|
159
|
+
[
|
|
160
|
+
TRANSPARENT,
|
|
161
|
+
ORANGE,
|
|
162
|
+
TRANSPARENT,
|
|
163
|
+
ORANGE,
|
|
164
|
+
TRANSPARENT,
|
|
165
|
+
ORANGE,
|
|
166
|
+
TRANSPARENT,
|
|
167
|
+
TRANSPARENT,
|
|
168
|
+
TRANSPARENT,
|
|
169
|
+
],
|
|
170
|
+
[
|
|
171
|
+
TRANSPARENT,
|
|
172
|
+
ORANGE,
|
|
173
|
+
TRANSPARENT,
|
|
174
|
+
ORANGE,
|
|
175
|
+
TRANSPARENT,
|
|
176
|
+
ORANGE,
|
|
177
|
+
TRANSPARENT,
|
|
178
|
+
ORANGE,
|
|
179
|
+
TRANSPARENT,
|
|
180
|
+
],
|
|
181
|
+
],
|
|
182
|
+
]
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def render_sprite(frame_index: int) -> Text:
|
|
186
|
+
frame = FRAMES[frame_index % len(FRAMES)]
|
|
187
|
+
text = Text()
|
|
188
|
+
|
|
189
|
+
for row in range(0, len(frame), 2):
|
|
190
|
+
upper = frame[row]
|
|
191
|
+
lower = frame[row + 1] if row + 1 < len(frame) else [None] * len(upper)
|
|
192
|
+
|
|
193
|
+
for upper_pixel, lower_pixel in zip(upper, lower, strict=True):
|
|
194
|
+
if upper_pixel is None and lower_pixel is None:
|
|
195
|
+
text.append(" ")
|
|
196
|
+
continue
|
|
197
|
+
if upper_pixel is None:
|
|
198
|
+
text.append("▄", style=Style(color=lower_pixel))
|
|
199
|
+
continue
|
|
200
|
+
if lower_pixel is None:
|
|
201
|
+
text.append("▀", style=Style(color=upper_pixel))
|
|
202
|
+
continue
|
|
203
|
+
text.append("▀", style=Style(color=upper_pixel, bgcolor=lower_pixel))
|
|
204
|
+
text.append("\n")
|
|
205
|
+
|
|
206
|
+
return text
|
ui/__init__.py
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# SPDX-License-Identifier: AGPL-3.0-only
|
|
2
|
+
# Copyright (C) 2026 lollapalooza <https://github.com/aqua5230>
|
|
3
|
+
#
|
|
4
|
+
# Part of "usage". Free software licensed under the GNU Affero General Public
|
|
5
|
+
# License v3.0 only; see the LICENSE file for full terms and the warranty disclaimer.
|