codex-statusline 0.7.0__tar.gz → 0.7.2__tar.gz
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.
- {codex_statusline-0.7.0 → codex_statusline-0.7.2}/PKG-INFO +10 -9
- {codex_statusline-0.7.0 → codex_statusline-0.7.2}/README.md +9 -8
- {codex_statusline-0.7.0 → codex_statusline-0.7.2}/codex_statusline.egg-info/PKG-INFO +10 -9
- {codex_statusline-0.7.0 → codex_statusline-0.7.2}/pyproject.toml +1 -1
- {codex_statusline-0.7.0 → codex_statusline-0.7.2}/src/__init__.py +1 -1
- {codex_statusline-0.7.0 → codex_statusline-0.7.2}/src/claude.py +20 -5
- {codex_statusline-0.7.0 → codex_statusline-0.7.2}/src/configure.py +3 -2
- {codex_statusline-0.7.0 → codex_statusline-0.7.2}/src/data.py +19 -18
- {codex_statusline-0.7.0 → codex_statusline-0.7.2}/src/manage.py +0 -19
- {codex_statusline-0.7.0 → codex_statusline-0.7.2}/src/render.py +18 -5
- {codex_statusline-0.7.0 → codex_statusline-0.7.2}/src/update.py +1 -1
- {codex_statusline-0.7.0 → codex_statusline-0.7.2}/tests/test_claude.py +34 -0
- {codex_statusline-0.7.0 → codex_statusline-0.7.2}/tests/test_configure.py +34 -12
- {codex_statusline-0.7.0 → codex_statusline-0.7.2}/tests/test_data.py +22 -1
- {codex_statusline-0.7.0 → codex_statusline-0.7.2}/tests/test_fish.py +0 -10
- {codex_statusline-0.7.0 → codex_statusline-0.7.2}/tests/test_render.py +21 -0
- {codex_statusline-0.7.0 → codex_statusline-0.7.2}/LICENSE +0 -0
- {codex_statusline-0.7.0 → codex_statusline-0.7.2}/codex_statusline.egg-info/SOURCES.txt +0 -0
- {codex_statusline-0.7.0 → codex_statusline-0.7.2}/codex_statusline.egg-info/dependency_links.txt +0 -0
- {codex_statusline-0.7.0 → codex_statusline-0.7.2}/codex_statusline.egg-info/entry_points.txt +0 -0
- {codex_statusline-0.7.0 → codex_statusline-0.7.2}/codex_statusline.egg-info/top_level.txt +0 -0
- {codex_statusline-0.7.0 → codex_statusline-0.7.2}/setup.cfg +0 -0
- {codex_statusline-0.7.0 → codex_statusline-0.7.2}/src/__main__.py +0 -0
- {codex_statusline-0.7.0 → codex_statusline-0.7.2}/src/cli.py +0 -0
- {codex_statusline-0.7.0 → codex_statusline-0.7.2}/src/doctor.py +0 -0
- {codex_statusline-0.7.0 → codex_statusline-0.7.2}/src/entry.py +0 -0
- {codex_statusline-0.7.0 → codex_statusline-0.7.2}/src/git.py +0 -0
- {codex_statusline-0.7.0 → codex_statusline-0.7.2}/src/hook.py +0 -0
- {codex_statusline-0.7.0 → codex_statusline-0.7.2}/src/launcher.py +0 -0
- {codex_statusline-0.7.0 → codex_statusline-0.7.2}/tests/test_auto_shells.py +0 -0
- {codex_statusline-0.7.0 → codex_statusline-0.7.2}/tests/test_doctor.py +0 -0
- {codex_statusline-0.7.0 → codex_statusline-0.7.2}/tests/test_git.py +0 -0
- {codex_statusline-0.7.0 → codex_statusline-0.7.2}/tests/test_line2.py +0 -0
- {codex_statusline-0.7.0 → codex_statusline-0.7.2}/tests/test_management.py +0 -0
- {codex_statusline-0.7.0 → codex_statusline-0.7.2}/tests/test_startup.py +0 -0
- {codex_statusline-0.7.0 → codex_statusline-0.7.2}/tests/test_tmux.py +0 -0
- {codex_statusline-0.7.0 → codex_statusline-0.7.2}/tests/test_update.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codex-statusline
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.2
|
|
4
4
|
Summary: Status bar for OpenAI Codex CLI and Claude Code: context usage, 5h and weekly quota with reset times, tokens, and cost at a glance. Local-only.
|
|
5
5
|
Author: Zimo Xu
|
|
6
6
|
License-Expression: MIT
|
|
@@ -65,16 +65,16 @@ Missing tmux? `brew install tmux` or `sudo apt install tmux`.
|
|
|
65
65
|
|
|
66
66
|
## Claude Code
|
|
67
67
|
|
|
68
|
-
The same bar works in [Claude Code](https://code.claude.com/docs/en/statusline): `cxbar install` sets it as Claude Code's `statusLine` when it finds `~/.claude`, so `claude` shows the same two lines, plus the session cost:
|
|
68
|
+
The same bar works in [Claude Code](https://code.claude.com/docs/en/statusline): `cxbar install` sets it as Claude Code's `statusLine` when it finds `~/.claude`, so `claude` shows the current model and the same two lines, plus the session cost:
|
|
69
69
|
|
|
70
70
|
```text
|
|
71
|
-
CTX USED ███░░░░░ 35% | 5h ██░░░░░░░░ 18% 3:46pm | week ██████░░░░ 62% Sat 1:46pm
|
|
71
|
+
Opus 5.5 | CTX USED ███░░░░░ 35% | 5h ██░░░░░░░░ 18% 3:46pm | week ██████░░░░ 62% Sat 1:46pm
|
|
72
72
|
in 1.2M · 94% cached · out 44.0k · ≈$1.84
|
|
73
73
|
```
|
|
74
74
|
|
|
75
75
|
- **Keeps what you have.** Already using another statusLine? Install leaves it alone; `cxbar install --claude` switches, and `cxbar uninstall` puts yours back.
|
|
76
76
|
- **No tmux needed** on the Claude side: Claude Code passes the numbers to the bar directly.
|
|
77
|
-
- **Every account.**
|
|
77
|
+
- **Every account.** Subscribers see 5h and weekly quota with reset times; plans without a 5-hour window (Claude Ultra, Codex Pro) show only the weekly quota. API-key users see context, tokens, and cost.
|
|
78
78
|
- **One config.** `cxbar config` changes the bar in both tools.
|
|
79
79
|
|
|
80
80
|
## Usage
|
|
@@ -127,13 +127,15 @@ Run `cxbar config` to set everything up in an interactive screen with a live, co
|
|
|
127
127
|
|
|
128
128
|
The second line has four parts:
|
|
129
129
|
|
|
130
|
-
- `usage` (on by default): tokens sent, how much of that hit the cache, and tokens generated.
|
|
130
|
+
- `usage` (on by default): tokens sent, how much of that hit the cache, and tokens generated. In Claude Code these are the tokens currently in context, as Claude Code reports them, with the session-wide cache hit rate; that is also why the `tokens` segment is off there by default.
|
|
131
131
|
- `cost` (on by default): the session's estimated cost at list price. Claude Code only.
|
|
132
132
|
- `git` (on by default): the branch and how many lines are added and removed since the last commit, counting new files the agent created too, e.g. `⎇ main +42 -10`. Hidden outside a git repo.
|
|
133
133
|
- `pace` (off by default): when your 5h and weekly quota run out at the rate you have used them so far, e.g. `week pace: runs out ~Tue 1:27am`, or `lasts to reset`.
|
|
134
134
|
|
|
135
135
|
Turn them all off to keep a single line.
|
|
136
136
|
|
|
137
|
+
The first-line `model` segment shows the current model, including its version when available. It is on by default in Claude Code and off in Codex, whose own footer already shows the model. Toggle or reorder it in `cxbar config`, or set `segments` per tool below. Codex does not supply session cost, so enabling `cost` cannot produce a dollar amount there. The `git` segment appears only when the session's working directory is inside a Git repository.
|
|
138
|
+
|
|
137
139
|
It writes `~/.config/codex-statusline/config.toml`, which you can also edit by hand:
|
|
138
140
|
|
|
139
141
|
```toml
|
|
@@ -147,6 +149,7 @@ update_check = true # show "↑ update available" in the bar when one is out
|
|
|
147
149
|
line2 = ["usage", "cost", "git"] # second line: any of "usage", "cost", "git", "pace"; [] for none
|
|
148
150
|
|
|
149
151
|
[claude]
|
|
152
|
+
segments = ["model", "ctx", "5h", "week"]
|
|
150
153
|
line2 = ["usage", "cost", "git", "pace"] # e.g. pace only in Claude Code
|
|
151
154
|
```
|
|
152
155
|
|
|
@@ -160,8 +163,6 @@ Invalid values are reported and fall back to defaults.
|
|
|
160
163
|
|
|
161
164
|
## FAQ
|
|
162
165
|
|
|
163
|
-
**How do I scroll or copy?** Exactly as in plain Codex: the mouse wheel scrolls the conversation, and text selection works the same way.
|
|
164
|
-
|
|
165
|
-
**Windows?** Not supported (tmux).
|
|
166
|
-
|
|
167
166
|
**Is this official?** No. It is a third-party tool built on a version-sensitive integration.
|
|
167
|
+
|
|
168
|
+
**Windows?** Not supported.
|
|
@@ -44,16 +44,16 @@ Missing tmux? `brew install tmux` or `sudo apt install tmux`.
|
|
|
44
44
|
|
|
45
45
|
## Claude Code
|
|
46
46
|
|
|
47
|
-
The same bar works in [Claude Code](https://code.claude.com/docs/en/statusline): `cxbar install` sets it as Claude Code's `statusLine` when it finds `~/.claude`, so `claude` shows the same two lines, plus the session cost:
|
|
47
|
+
The same bar works in [Claude Code](https://code.claude.com/docs/en/statusline): `cxbar install` sets it as Claude Code's `statusLine` when it finds `~/.claude`, so `claude` shows the current model and the same two lines, plus the session cost:
|
|
48
48
|
|
|
49
49
|
```text
|
|
50
|
-
CTX USED ███░░░░░ 35% | 5h ██░░░░░░░░ 18% 3:46pm | week ██████░░░░ 62% Sat 1:46pm
|
|
50
|
+
Opus 5.5 | CTX USED ███░░░░░ 35% | 5h ██░░░░░░░░ 18% 3:46pm | week ██████░░░░ 62% Sat 1:46pm
|
|
51
51
|
in 1.2M · 94% cached · out 44.0k · ≈$1.84
|
|
52
52
|
```
|
|
53
53
|
|
|
54
54
|
- **Keeps what you have.** Already using another statusLine? Install leaves it alone; `cxbar install --claude` switches, and `cxbar uninstall` puts yours back.
|
|
55
55
|
- **No tmux needed** on the Claude side: Claude Code passes the numbers to the bar directly.
|
|
56
|
-
- **Every account.**
|
|
56
|
+
- **Every account.** Subscribers see 5h and weekly quota with reset times; plans without a 5-hour window (Claude Ultra, Codex Pro) show only the weekly quota. API-key users see context, tokens, and cost.
|
|
57
57
|
- **One config.** `cxbar config` changes the bar in both tools.
|
|
58
58
|
|
|
59
59
|
## Usage
|
|
@@ -106,13 +106,15 @@ Run `cxbar config` to set everything up in an interactive screen with a live, co
|
|
|
106
106
|
|
|
107
107
|
The second line has four parts:
|
|
108
108
|
|
|
109
|
-
- `usage` (on by default): tokens sent, how much of that hit the cache, and tokens generated.
|
|
109
|
+
- `usage` (on by default): tokens sent, how much of that hit the cache, and tokens generated. In Claude Code these are the tokens currently in context, as Claude Code reports them, with the session-wide cache hit rate; that is also why the `tokens` segment is off there by default.
|
|
110
110
|
- `cost` (on by default): the session's estimated cost at list price. Claude Code only.
|
|
111
111
|
- `git` (on by default): the branch and how many lines are added and removed since the last commit, counting new files the agent created too, e.g. `⎇ main +42 -10`. Hidden outside a git repo.
|
|
112
112
|
- `pace` (off by default): when your 5h and weekly quota run out at the rate you have used them so far, e.g. `week pace: runs out ~Tue 1:27am`, or `lasts to reset`.
|
|
113
113
|
|
|
114
114
|
Turn them all off to keep a single line.
|
|
115
115
|
|
|
116
|
+
The first-line `model` segment shows the current model, including its version when available. It is on by default in Claude Code and off in Codex, whose own footer already shows the model. Toggle or reorder it in `cxbar config`, or set `segments` per tool below. Codex does not supply session cost, so enabling `cost` cannot produce a dollar amount there. The `git` segment appears only when the session's working directory is inside a Git repository.
|
|
117
|
+
|
|
116
118
|
It writes `~/.config/codex-statusline/config.toml`, which you can also edit by hand:
|
|
117
119
|
|
|
118
120
|
```toml
|
|
@@ -126,6 +128,7 @@ update_check = true # show "↑ update available" in the bar when one is out
|
|
|
126
128
|
line2 = ["usage", "cost", "git"] # second line: any of "usage", "cost", "git", "pace"; [] for none
|
|
127
129
|
|
|
128
130
|
[claude]
|
|
131
|
+
segments = ["model", "ctx", "5h", "week"]
|
|
129
132
|
line2 = ["usage", "cost", "git", "pace"] # e.g. pace only in Claude Code
|
|
130
133
|
```
|
|
131
134
|
|
|
@@ -139,8 +142,6 @@ Invalid values are reported and fall back to defaults.
|
|
|
139
142
|
|
|
140
143
|
## FAQ
|
|
141
144
|
|
|
142
|
-
**How do I scroll or copy?** Exactly as in plain Codex: the mouse wheel scrolls the conversation, and text selection works the same way.
|
|
143
|
-
|
|
144
|
-
**Windows?** Not supported (tmux).
|
|
145
|
-
|
|
146
145
|
**Is this official?** No. It is a third-party tool built on a version-sensitive integration.
|
|
146
|
+
|
|
147
|
+
**Windows?** Not supported.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codex-statusline
|
|
3
|
-
Version: 0.7.
|
|
3
|
+
Version: 0.7.2
|
|
4
4
|
Summary: Status bar for OpenAI Codex CLI and Claude Code: context usage, 5h and weekly quota with reset times, tokens, and cost at a glance. Local-only.
|
|
5
5
|
Author: Zimo Xu
|
|
6
6
|
License-Expression: MIT
|
|
@@ -65,16 +65,16 @@ Missing tmux? `brew install tmux` or `sudo apt install tmux`.
|
|
|
65
65
|
|
|
66
66
|
## Claude Code
|
|
67
67
|
|
|
68
|
-
The same bar works in [Claude Code](https://code.claude.com/docs/en/statusline): `cxbar install` sets it as Claude Code's `statusLine` when it finds `~/.claude`, so `claude` shows the same two lines, plus the session cost:
|
|
68
|
+
The same bar works in [Claude Code](https://code.claude.com/docs/en/statusline): `cxbar install` sets it as Claude Code's `statusLine` when it finds `~/.claude`, so `claude` shows the current model and the same two lines, plus the session cost:
|
|
69
69
|
|
|
70
70
|
```text
|
|
71
|
-
CTX USED ███░░░░░ 35% | 5h ██░░░░░░░░ 18% 3:46pm | week ██████░░░░ 62% Sat 1:46pm
|
|
71
|
+
Opus 5.5 | CTX USED ███░░░░░ 35% | 5h ██░░░░░░░░ 18% 3:46pm | week ██████░░░░ 62% Sat 1:46pm
|
|
72
72
|
in 1.2M · 94% cached · out 44.0k · ≈$1.84
|
|
73
73
|
```
|
|
74
74
|
|
|
75
75
|
- **Keeps what you have.** Already using another statusLine? Install leaves it alone; `cxbar install --claude` switches, and `cxbar uninstall` puts yours back.
|
|
76
76
|
- **No tmux needed** on the Claude side: Claude Code passes the numbers to the bar directly.
|
|
77
|
-
- **Every account.**
|
|
77
|
+
- **Every account.** Subscribers see 5h and weekly quota with reset times; plans without a 5-hour window (Claude Ultra, Codex Pro) show only the weekly quota. API-key users see context, tokens, and cost.
|
|
78
78
|
- **One config.** `cxbar config` changes the bar in both tools.
|
|
79
79
|
|
|
80
80
|
## Usage
|
|
@@ -127,13 +127,15 @@ Run `cxbar config` to set everything up in an interactive screen with a live, co
|
|
|
127
127
|
|
|
128
128
|
The second line has four parts:
|
|
129
129
|
|
|
130
|
-
- `usage` (on by default): tokens sent, how much of that hit the cache, and tokens generated.
|
|
130
|
+
- `usage` (on by default): tokens sent, how much of that hit the cache, and tokens generated. In Claude Code these are the tokens currently in context, as Claude Code reports them, with the session-wide cache hit rate; that is also why the `tokens` segment is off there by default.
|
|
131
131
|
- `cost` (on by default): the session's estimated cost at list price. Claude Code only.
|
|
132
132
|
- `git` (on by default): the branch and how many lines are added and removed since the last commit, counting new files the agent created too, e.g. `⎇ main +42 -10`. Hidden outside a git repo.
|
|
133
133
|
- `pace` (off by default): when your 5h and weekly quota run out at the rate you have used them so far, e.g. `week pace: runs out ~Tue 1:27am`, or `lasts to reset`.
|
|
134
134
|
|
|
135
135
|
Turn them all off to keep a single line.
|
|
136
136
|
|
|
137
|
+
The first-line `model` segment shows the current model, including its version when available. It is on by default in Claude Code and off in Codex, whose own footer already shows the model. Toggle or reorder it in `cxbar config`, or set `segments` per tool below. Codex does not supply session cost, so enabling `cost` cannot produce a dollar amount there. The `git` segment appears only when the session's working directory is inside a Git repository.
|
|
138
|
+
|
|
137
139
|
It writes `~/.config/codex-statusline/config.toml`, which you can also edit by hand:
|
|
138
140
|
|
|
139
141
|
```toml
|
|
@@ -147,6 +149,7 @@ update_check = true # show "↑ update available" in the bar when one is out
|
|
|
147
149
|
line2 = ["usage", "cost", "git"] # second line: any of "usage", "cost", "git", "pace"; [] for none
|
|
148
150
|
|
|
149
151
|
[claude]
|
|
152
|
+
segments = ["model", "ctx", "5h", "week"]
|
|
150
153
|
line2 = ["usage", "cost", "git", "pace"] # e.g. pace only in Claude Code
|
|
151
154
|
```
|
|
152
155
|
|
|
@@ -160,8 +163,6 @@ Invalid values are reported and fall back to defaults.
|
|
|
160
163
|
|
|
161
164
|
## FAQ
|
|
162
165
|
|
|
163
|
-
**How do I scroll or copy?** Exactly as in plain Codex: the mouse wheel scrolls the conversation, and text selection works the same way.
|
|
164
|
-
|
|
165
|
-
**Windows?** Not supported (tmux).
|
|
166
|
-
|
|
167
166
|
**Is this official?** No. It is a third-party tool built on a version-sensitive integration.
|
|
167
|
+
|
|
168
|
+
**Windows?** Not supported.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "codex-statusline"
|
|
3
|
-
version = "0.7.
|
|
3
|
+
version = "0.7.2"
|
|
4
4
|
description = "Status bar for OpenAI Codex CLI and Claude Code: context usage, 5h and weekly quota with reset times, tokens, and cost at a glance. Local-only."
|
|
5
5
|
requires-python = ">=3.11"
|
|
6
6
|
license = "MIT"
|
|
@@ -22,11 +22,21 @@ def _num(value: Any) -> float | None:
|
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
def state_from(data: dict, now: float) -> dict:
|
|
25
|
+
model = data.get("model") if isinstance(data.get("model"), dict) else {}
|
|
26
|
+
model_id = model.get("id")
|
|
27
|
+
name = model.get("display_name")
|
|
28
|
+
name = name.strip() if isinstance(name, str) else ""
|
|
29
|
+
if isinstance(model_id, str):
|
|
30
|
+
match = re.fullmatch(r"claude-([a-z]+)-(\d+)-(\d+)(?:-\d{8})?(\[1m\])?", model_id)
|
|
31
|
+
if match and (not name or name.casefold() == match[1]):
|
|
32
|
+
name = f"{match[1].capitalize()} {match[2]}.{match[3]}{match[4] or ''}"
|
|
33
|
+
name = name or model_id
|
|
25
34
|
window = data.get("context_window") if isinstance(data.get("context_window"), dict) else {}
|
|
26
35
|
limits = data.get("rate_limits") if isinstance(data.get("rate_limits"), dict) else {}
|
|
27
36
|
cost = data.get("cost") if isinstance(data.get("cost"), dict) else {}
|
|
28
37
|
state: dict[str, Any] = {
|
|
29
38
|
"now": now,
|
|
39
|
+
"model": name,
|
|
30
40
|
"context_used": _num(window.get("used_percentage")),
|
|
31
41
|
"quota_optional": True, # only subscriptions report rate limits
|
|
32
42
|
}
|
|
@@ -46,13 +56,18 @@ def state_from(data: dict, now: float) -> dict:
|
|
|
46
56
|
given, output = _num(window.get("total_input_tokens")), _num(window.get("total_output_tokens"))
|
|
47
57
|
if given is not None and output is not None:
|
|
48
58
|
state["tokens"] = given + output
|
|
49
|
-
#
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
59
|
+
# Prefer the session-wide hit ratio; older Claude Code only has the latest request's split.
|
|
60
|
+
cache = data.get("prompt_cache") if isinstance(data.get("prompt_cache"), dict) else {}
|
|
61
|
+
ratio = _num(cache.get("hit_ratio"))
|
|
62
|
+
if ratio is None:
|
|
63
|
+
last = window.get("current_usage")
|
|
64
|
+
last = last if isinstance(last, dict) else {}
|
|
65
|
+
keys = ("input_tokens", "cache_read_input_tokens", "cache_creation_input_tokens")
|
|
66
|
+
parts = [_num(last.get(k)) or 0.0 for k in keys]
|
|
67
|
+
ratio = parts[1] / sum(parts) if sum(parts) else None
|
|
53
68
|
state["usage"] = {
|
|
54
69
|
"input_tokens": given,
|
|
55
|
-
"cached_input_tokens": given *
|
|
70
|
+
"cached_input_tokens": given * min(ratio, 1.0) if ratio is not None else None,
|
|
56
71
|
"output_tokens": output,
|
|
57
72
|
}
|
|
58
73
|
state["cost"] = _num(cost.get("total_cost_usd"))
|
|
@@ -6,7 +6,6 @@ import re
|
|
|
6
6
|
import time
|
|
7
7
|
|
|
8
8
|
from .render import (
|
|
9
|
-
DEFAULT_SEGMENTS,
|
|
10
9
|
LINE2_SEGMENTS,
|
|
11
10
|
TARGETS,
|
|
12
11
|
THEMES,
|
|
@@ -22,6 +21,7 @@ SEGMENT_NAMES = {
|
|
|
22
21
|
"5h": "5-hour quota",
|
|
23
22
|
"week": "weekly quota",
|
|
24
23
|
"tokens": "session tokens",
|
|
24
|
+
"model": "current model",
|
|
25
25
|
}
|
|
26
26
|
LINE2_NAMES = {
|
|
27
27
|
"usage": "tokens in / cached / out",
|
|
@@ -44,7 +44,7 @@ HELP = "↑↓ move space toggle ←→ change J/K reorder Tab switch tool
|
|
|
44
44
|
|
|
45
45
|
def model_from(cfg: dict, target: str = "claude") -> dict:
|
|
46
46
|
enabled = [name for name in cfg["segments"] if name in SEGMENT_NAMES]
|
|
47
|
-
order = enabled + [name for name in
|
|
47
|
+
order = enabled + [name for name in SEGMENT_NAMES if name not in enabled]
|
|
48
48
|
line2_order = LINE2_FOR[target]
|
|
49
49
|
return {
|
|
50
50
|
**{key: cfg[key] for key in SETTINGS},
|
|
@@ -150,6 +150,7 @@ def rows_text(model: dict) -> list[str]:
|
|
|
150
150
|
def sample_state(target: str = "codex") -> dict:
|
|
151
151
|
now = time.time()
|
|
152
152
|
state = {
|
|
153
|
+
"model": "gpt-model" if target == "codex" else "Opus 5.5",
|
|
153
154
|
"context_used": 35,
|
|
154
155
|
"tokens": 1_234_567,
|
|
155
156
|
"usage": {
|
|
@@ -99,6 +99,16 @@ def _timestamp(value: Any) -> float | None:
|
|
|
99
99
|
return None
|
|
100
100
|
|
|
101
101
|
|
|
102
|
+
def _window_label(minutes: Any) -> str | None:
|
|
103
|
+
"""Name a window by its length, within the same 5% tolerance Codex's own TUI uses."""
|
|
104
|
+
if not _count(minutes):
|
|
105
|
+
return None
|
|
106
|
+
for base, label in ((300, "5h"), (10080, "weekly")):
|
|
107
|
+
if abs(minutes - base) <= base * 0.05:
|
|
108
|
+
return label
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
|
|
102
112
|
def _empty_quotas() -> dict[str, dict[str, float | None]]:
|
|
103
113
|
return {
|
|
104
114
|
"5h": {"remaining": None, "reset_at": None, "observed_at": None},
|
|
@@ -168,12 +178,16 @@ class LogReader:
|
|
|
168
178
|
if not isinstance(rate_limits, dict):
|
|
169
179
|
return
|
|
170
180
|
|
|
171
|
-
#
|
|
181
|
+
# Model-specific buckets (e.g. "codex_other") are not the account-wide quota.
|
|
182
|
+
limit_id = rate_limits.get("limit_id")
|
|
183
|
+
if limit_id is not None and limit_id != "codex":
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
# Slots vary by plan (e.g. Pro has no 5h window, so weekly may sit in "primary"),
|
|
172
187
|
# so identify each quota by its window length, not by its key.
|
|
173
|
-
windows = {300: "5h", 10080: "weekly"}
|
|
174
188
|
for key in ("primary", "secondary"):
|
|
175
189
|
quota = rate_limits.get(key)
|
|
176
|
-
label =
|
|
190
|
+
label = _window_label(quota.get("window_minutes")) if isinstance(quota, dict) else None
|
|
177
191
|
if label is None:
|
|
178
192
|
continue
|
|
179
193
|
used = quota.get("used_percent")
|
|
@@ -233,14 +247,8 @@ class LogReader:
|
|
|
233
247
|
probe_stream.seek(self._offset - len(self._probe))
|
|
234
248
|
overwritten = probe_stream.read(len(self._probe)) != self._probe
|
|
235
249
|
if identity != self._identity or stat.st_size < self._offset or overwritten:
|
|
250
|
+
self._reset(path)
|
|
236
251
|
self._identity = identity
|
|
237
|
-
self._offset = 0
|
|
238
|
-
self._partial = b""
|
|
239
|
-
self._discard_until_newline = False
|
|
240
|
-
self._probe = b""
|
|
241
|
-
self._quotas = _empty_quotas()
|
|
242
|
-
self._tokens = None
|
|
243
|
-
self._usage = None
|
|
244
252
|
if stat.st_size > _MAX_READ_BYTES:
|
|
245
253
|
# Start at a bounded tail boundary, discarding its possibly
|
|
246
254
|
# incomplete first line. Current quota events remain
|
|
@@ -268,14 +276,7 @@ class LogReader:
|
|
|
268
276
|
except FileNotFoundError:
|
|
269
277
|
# A missing bound file has no metadata yet. Retain the selected
|
|
270
278
|
# path, but a later appearance will be picked up by identity reset.
|
|
271
|
-
self.
|
|
272
|
-
self._offset = 0
|
|
273
|
-
self._partial = b""
|
|
274
|
-
self._discard_until_newline = False
|
|
275
|
-
self._probe = b""
|
|
276
|
-
self._quotas = _empty_quotas()
|
|
277
|
-
self._tokens = None
|
|
278
|
-
self._usage = None
|
|
279
|
+
self._reset(path)
|
|
279
280
|
except OSError:
|
|
280
281
|
# Avoid exporting filesystem details or turning transient I/O into
|
|
281
282
|
# fabricated quota data; previously observed metadata remains.
|
|
@@ -5,7 +5,6 @@ import json
|
|
|
5
5
|
import os
|
|
6
6
|
import shlex
|
|
7
7
|
import shutil
|
|
8
|
-
import subprocess
|
|
9
8
|
import sys
|
|
10
9
|
import time
|
|
11
10
|
import tomllib
|
|
@@ -19,24 +18,6 @@ END = "# <<< codex-statusline <<<"
|
|
|
19
18
|
STATUS_MESSAGE = "codex-statusline binding"
|
|
20
19
|
|
|
21
20
|
|
|
22
|
-
def detected_shell():
|
|
23
|
-
# $SHELL can remain zsh even when Ghostty's configured command is fish.
|
|
24
|
-
try:
|
|
25
|
-
proc = subprocess.run(
|
|
26
|
-
["ps", "-p", str(os.getppid()), "-o", "comm="],
|
|
27
|
-
capture_output=True,
|
|
28
|
-
text=True,
|
|
29
|
-
timeout=1,
|
|
30
|
-
)
|
|
31
|
-
name = Path(proc.stdout.strip()).name.lstrip("-")
|
|
32
|
-
if name in ("fish", "zsh", "bash"):
|
|
33
|
-
return name
|
|
34
|
-
except (OSError, subprocess.TimeoutExpired):
|
|
35
|
-
pass
|
|
36
|
-
name = Path(os.environ.get("SHELL", "/bin/zsh")).name
|
|
37
|
-
return name if name in ("fish", "zsh", "bash") else "zsh"
|
|
38
|
-
|
|
39
|
-
|
|
40
21
|
def available_shells():
|
|
41
22
|
"""Install for every supported shell available on this machine."""
|
|
42
23
|
return [name for name in ("bash", "zsh", "fish") if shutil.which(name)]
|
|
@@ -53,6 +53,9 @@ def load_config(path: str | os.PathLike | None = None, target: str = "codex") ->
|
|
|
53
53
|
if path is None:
|
|
54
54
|
path = config_path()
|
|
55
55
|
config = dict(_CONFIG_DEFAULTS)
|
|
56
|
+
if target == "claude":
|
|
57
|
+
# Claude's token counts describe the current context only, which ctx already shows.
|
|
58
|
+
config["segments"] = ["model", "ctx", "5h", "week"]
|
|
56
59
|
try:
|
|
57
60
|
raw = tomllib.loads(Path(path).read_text())
|
|
58
61
|
except FileNotFoundError:
|
|
@@ -62,7 +65,9 @@ def load_config(path: str | os.PathLike | None = None, target: str = "codex") ->
|
|
|
62
65
|
raw = {}
|
|
63
66
|
checks = {
|
|
64
67
|
"segments": lambda v: (
|
|
65
|
-
isinstance(v, list)
|
|
68
|
+
isinstance(v, list)
|
|
69
|
+
and v
|
|
70
|
+
and all(x in ("model", "ctx", "5h", "week", "tokens") for x in v)
|
|
66
71
|
),
|
|
67
72
|
"theme": lambda v: v in THEMES,
|
|
68
73
|
"ascii": lambda v: isinstance(v, bool),
|
|
@@ -160,13 +165,12 @@ def _pct(value: Any) -> str:
|
|
|
160
165
|
return "--" if number is None else f"{number}%"
|
|
161
166
|
|
|
162
167
|
|
|
163
|
-
def _bar(value: Any, slots: int, ascii_only: bool
|
|
168
|
+
def _bar(value: Any, slots: int, ascii_only: bool) -> str:
|
|
164
169
|
number = _percent(value)
|
|
165
170
|
if number is None:
|
|
166
171
|
symbol = "-" if ascii_only else "·"
|
|
167
172
|
return symbol * slots
|
|
168
|
-
|
|
169
|
-
filled = round(ratio * slots / 100)
|
|
173
|
+
filled = round(number * slots / 100)
|
|
170
174
|
if ascii_only:
|
|
171
175
|
return "=" * filled + "-" * (slots - filled)
|
|
172
176
|
return "█" * filled + "░" * (slots - filled)
|
|
@@ -199,7 +203,7 @@ def _quota_values(
|
|
|
199
203
|
left = "--" if remaining is None else f"{remaining}%"
|
|
200
204
|
reset = _reset(data.get("reset_at"), now, weekly)
|
|
201
205
|
compact = f"{label} {left}"
|
|
202
|
-
bar = _bar(remaining, 10, ascii_only
|
|
206
|
+
bar = _bar(remaining, 10, ascii_only)
|
|
203
207
|
detail = f"{label} {bar} {left} {reset}".rstrip()
|
|
204
208
|
return detail, compact, remaining
|
|
205
209
|
|
|
@@ -369,6 +373,11 @@ def render(
|
|
|
369
373
|
"week": (qwdetail, qwcompact, _quota_role(qwvalue, warn_at, crit_at)),
|
|
370
374
|
"tokens": (tok, tok, "accent"),
|
|
371
375
|
}
|
|
376
|
+
model = state.get("model")
|
|
377
|
+
if isinstance(model, str) and model.strip():
|
|
378
|
+
text = _safe(model, ascii_only, tmux and not ansi).strip()
|
|
379
|
+
if text:
|
|
380
|
+
known["model"] = (text, text, "accent")
|
|
372
381
|
# Plans without a 5h (or weekly) limit report only the other window: hide the missing one.
|
|
373
382
|
if (q5value is None) != (qwvalue is None):
|
|
374
383
|
del known["5h" if q5value is None else "week"]
|
|
@@ -378,6 +387,10 @@ def render(
|
|
|
378
387
|
parts = [known[name] for name in (segments or DEFAULT_SEGMENTS) if name in known] or [
|
|
379
388
|
known["ctx"]
|
|
380
389
|
]
|
|
390
|
+
parts = [
|
|
391
|
+
(detail if i == 0 else detail.lstrip(), compact if i == 0 else compact.lstrip(), role)
|
|
392
|
+
for i, (detail, compact, role) in enumerate(parts)
|
|
393
|
+
]
|
|
381
394
|
update = state.get("update")
|
|
382
395
|
hint = None
|
|
383
396
|
if isinstance(update, str) and re.fullmatch(r"\d+(\.\d+)*", update):
|
|
@@ -122,7 +122,7 @@ def check(latest=latest_version) -> int:
|
|
|
122
122
|
|
|
123
123
|
def update() -> int:
|
|
124
124
|
command = upgrade_command()
|
|
125
|
-
print("$ " + " ".join(command))
|
|
125
|
+
print("$ " + " ".join(command), flush=True) # before the tool's own output
|
|
126
126
|
if not shutil.which(command[0]):
|
|
127
127
|
print(
|
|
128
128
|
f"codex-statusline: {command[0]} not found; run the command above yourself.",
|
|
@@ -15,6 +15,7 @@ from src.claude import state_from, statusline
|
|
|
15
15
|
|
|
16
16
|
NOW = time.time()
|
|
17
17
|
SUBSCRIBER = {
|
|
18
|
+
"model": {"id": "claude-opus-5-5", "display_name": "Opus"},
|
|
18
19
|
"context_window": {
|
|
19
20
|
"used_percentage": 35,
|
|
20
21
|
"total_input_tokens": 1_190_000,
|
|
@@ -54,6 +55,7 @@ class ClaudeStatuslineTests(unittest.TestCase):
|
|
|
54
55
|
self.assertIn("\x1b[38;5;", out) # ANSI colors for Claude Code
|
|
55
56
|
plain = ANSI.sub("", out).splitlines()
|
|
56
57
|
self.assertEqual(len(plain), 2)
|
|
58
|
+
self.assertTrue(plain[0].startswith("Opus 5.5 | CTX USED"), plain[0])
|
|
57
59
|
self.assertIn("CTX USED", plain[0])
|
|
58
60
|
self.assertIn("5h ██░░░░░░░░ 18%", plain[0])
|
|
59
61
|
self.assertIn("in 1.2M · 94% cached · out 44.0k · ≈$1.84", plain[1])
|
|
@@ -73,6 +75,21 @@ class ClaudeStatuslineTests(unittest.TestCase):
|
|
|
73
75
|
self.assertNotIn("cached", plain)
|
|
74
76
|
self.assertIn("in 52.0k · out 3.1k · ≈$0.43", plain)
|
|
75
77
|
|
|
78
|
+
def test_plan_without_5h_window_shows_weekly_only(self):
|
|
79
|
+
data = dict(SUBSCRIBER, rate_limits={"seven_day": SUBSCRIBER["rate_limits"]["seven_day"]})
|
|
80
|
+
plain = ANSI.sub("", run_statusline(data, self.home))
|
|
81
|
+
self.assertNotIn("5h", plain)
|
|
82
|
+
self.assertIn("week", plain)
|
|
83
|
+
|
|
84
|
+
def test_tokens_segment_off_by_default(self):
|
|
85
|
+
self.assertNotIn("tok ", ANSI.sub("", run_statusline(SUBSCRIBER, self.home)))
|
|
86
|
+
|
|
87
|
+
def test_session_hit_ratio_preferred_over_last_request(self):
|
|
88
|
+
data = dict(SUBSCRIBER, prompt_cache={"hit_ratio": 0.5})
|
|
89
|
+
self.assertIn("50% cached", ANSI.sub("", run_statusline(data, self.home)))
|
|
90
|
+
data = dict(SUBSCRIBER, prompt_cache={"hit_ratio": None})
|
|
91
|
+
self.assertIn("94% cached", ANSI.sub("", run_statusline(data, self.home)))
|
|
92
|
+
|
|
76
93
|
def test_bad_input_still_prints_a_bar(self):
|
|
77
94
|
for data in ("not json", "[1, 2]", ""):
|
|
78
95
|
self.assertIn("CTX USED", run_statusline(data, self.home))
|
|
@@ -83,6 +100,23 @@ class ClaudeStatuslineTests(unittest.TestCase):
|
|
|
83
100
|
self.assertEqual(state["quotas"]["weekly"]["reset_at"], NOW + 200_000)
|
|
84
101
|
self.assertEqual(state["tokens"], 1_234_000)
|
|
85
102
|
|
|
103
|
+
def test_model_name_fallback_and_configurable_segment(self):
|
|
104
|
+
for model, expected in (
|
|
105
|
+
({"id": "claude-opus-5-5", "display_name": "Opus"}, "Opus 5.5"),
|
|
106
|
+
({"id": "claude-sonnet-4-5-20250929"}, "Sonnet 4.5"),
|
|
107
|
+
({"id": "claude-opus-5-5[1m]", "display_name": "Opus"}, "Opus 5.5[1m]"),
|
|
108
|
+
({"id": "custom-model", "display_name": "My Model"}, "My Model"),
|
|
109
|
+
({"id": "custom-model"}, "custom-model"),
|
|
110
|
+
(None, ""),
|
|
111
|
+
):
|
|
112
|
+
with self.subTest(model=model):
|
|
113
|
+
self.assertEqual(state_from({"model": model}, NOW)["model"], expected)
|
|
114
|
+
config = self.home / "cfg/codex-statusline/config.toml"
|
|
115
|
+
config.parent.mkdir(parents=True)
|
|
116
|
+
config.write_text('[claude]\nsegments = ["ctx", "tokens"]\n')
|
|
117
|
+
plain = ANSI.sub("", run_statusline(SUBSCRIBER, self.home))
|
|
118
|
+
self.assertNotIn("Opus", plain)
|
|
119
|
+
|
|
86
120
|
|
|
87
121
|
class ClaudeInstallTests(unittest.TestCase):
|
|
88
122
|
def setUp(self):
|
|
@@ -3,7 +3,7 @@ import unittest
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
from unittest.mock import patch
|
|
5
5
|
|
|
6
|
-
from src.configure import apply, dump_toml, model_from, rows_text, to_config
|
|
6
|
+
from src.configure import SETTINGS, apply, dump_toml, model_from, rows_text, to_config
|
|
7
7
|
from src.render import load_config
|
|
8
8
|
|
|
9
9
|
|
|
@@ -17,15 +17,17 @@ class ConfigureTests(unittest.TestCase):
|
|
|
17
17
|
row = apply(m, 3, "space") # turn tokens off
|
|
18
18
|
row = apply(m, 1, "J") # move 5h below week
|
|
19
19
|
self.assertEqual(row, 2)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
apply(m,
|
|
23
|
-
apply(m,
|
|
24
|
-
apply(m,
|
|
25
|
-
apply(m,
|
|
26
|
-
apply(m,
|
|
27
|
-
apply(m,
|
|
28
|
-
apply(m,
|
|
20
|
+
line2 = len(m["order"])
|
|
21
|
+
settings = line2 + len(m["line2_order"])
|
|
22
|
+
apply(m, settings, "right") # theme auto -> dark
|
|
23
|
+
apply(m, settings + 2, "left") # warn_at 40 -> 35
|
|
24
|
+
apply(m, settings + 3, "left") # crit_at 20 -> 15
|
|
25
|
+
apply(m, settings + 3, "left") # 15 -> 10
|
|
26
|
+
apply(m, settings + 4, "space") # update notice off
|
|
27
|
+
apply(m, line2, "space") # usage off
|
|
28
|
+
apply(m, line2 + 1, "space") # cost off
|
|
29
|
+
apply(m, line2 + 2, "space") # git off
|
|
30
|
+
apply(m, line2 + 3, "space") # pace on
|
|
29
31
|
cfg = to_config(m)
|
|
30
32
|
self.assertEqual(cfg["line2"], ["pace"])
|
|
31
33
|
self.assertEqual(cfg["segments"], ["ctx", "week", "5h"])
|
|
@@ -48,6 +50,23 @@ class ConfigureTests(unittest.TestCase):
|
|
|
48
50
|
|
|
49
51
|
|
|
50
52
|
class TwoToolTests(unittest.TestCase):
|
|
53
|
+
def test_model_toggle_reorder_and_roundtrip(self):
|
|
54
|
+
from src.configure import dump_sections
|
|
55
|
+
|
|
56
|
+
with patch.dict("os.environ", {}, clear=True):
|
|
57
|
+
model = model_from(load_config("/nonexistent.toml", "claude"), "claude")
|
|
58
|
+
self.assertIn("model", model["enabled"])
|
|
59
|
+
row = apply(model, model["order"].index("model"), "J")
|
|
60
|
+
self.assertEqual(row, 1)
|
|
61
|
+
apply(model, row, "space")
|
|
62
|
+
text = dump_sections({"claude": to_config(model)})
|
|
63
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
64
|
+
path = Path(tmp) / "config.toml"
|
|
65
|
+
path.write_text(text)
|
|
66
|
+
saved = load_config(path, "claude")
|
|
67
|
+
self.assertNotIn("model", saved["segments"])
|
|
68
|
+
self.assertEqual(saved, to_config(model))
|
|
69
|
+
|
|
51
70
|
def test_each_tool_saves_and_loads_its_own_section(self):
|
|
52
71
|
from src.configure import dump_sections
|
|
53
72
|
|
|
@@ -65,7 +84,9 @@ class TwoToolTests(unittest.TestCase):
|
|
|
65
84
|
self.assertNotIn("tokens", load_config(path, "codex")["segments"])
|
|
66
85
|
self.assertEqual(load_config(path, "codex")["theme"], "auto")
|
|
67
86
|
self.assertEqual(load_config(path, "claude")["theme"], "light")
|
|
68
|
-
self.
|
|
87
|
+
self.assertEqual(
|
|
88
|
+
load_config(path, "claude")["segments"], ["model", "ctx", "5h", "week"]
|
|
89
|
+
)
|
|
69
90
|
|
|
70
91
|
def test_old_flat_config_applies_to_both_tools(self):
|
|
71
92
|
with tempfile.TemporaryDirectory() as tmp:
|
|
@@ -94,8 +115,9 @@ class TwoToolTests(unittest.TestCase):
|
|
|
94
115
|
|
|
95
116
|
with patch.dict("os.environ", {}, clear=True):
|
|
96
117
|
model = model_from(load_config("/nonexistent.toml", "codex"), "codex")
|
|
118
|
+
row = len(model["order"]) + len(model["line2_order"]) + SETTINGS.index("crit_at")
|
|
97
119
|
for _ in range(30):
|
|
98
|
-
apply(model,
|
|
120
|
+
apply(model, row, "left") # crit_at all the way down
|
|
99
121
|
self.assertEqual(model["crit_at"], 0)
|
|
100
122
|
text = dump_sections({"codex": to_config(model)})
|
|
101
123
|
self.assertEqual(text, "[codex]\ncrit_at = 0\n")
|
|
@@ -122,7 +122,7 @@ class LogReaderTests(unittest.TestCase):
|
|
|
122
122
|
self.assertIsNone(reader.update(str(second))["tokens"])
|
|
123
123
|
|
|
124
124
|
def test_weekly_only_plan_in_primary_slot(self):
|
|
125
|
-
#
|
|
125
|
+
# Plans without a 5h window (e.g. Pro) report only the weekly one, in "primary".
|
|
126
126
|
event = token_event(1000, primary_used=11, primary_window=10080)
|
|
127
127
|
event["payload"]["info"]["rate_limits"]["secondary"] = None
|
|
128
128
|
with tempfile.TemporaryDirectory() as tmp:
|
|
@@ -132,6 +132,27 @@ class LogReaderTests(unittest.TestCase):
|
|
|
132
132
|
self.assertEqual(quotas["weekly"]["remaining"], 89.0)
|
|
133
133
|
self.assertIsNone(quotas["5h"]["remaining"])
|
|
134
134
|
|
|
135
|
+
def test_windows_match_within_five_percent(self):
|
|
136
|
+
event = token_event(1000, 10, 20, primary_window=299, secondary_window=10100)
|
|
137
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
138
|
+
path = Path(tmp) / "s.jsonl"
|
|
139
|
+
path.write_bytes(line(event))
|
|
140
|
+
quotas = LogReader().update(str(path))["quotas"]
|
|
141
|
+
self.assertEqual(quotas["5h"]["remaining"], 90.0)
|
|
142
|
+
self.assertEqual(quotas["weekly"]["remaining"], 80.0)
|
|
143
|
+
|
|
144
|
+
def test_model_specific_limit_bucket_is_ignored(self):
|
|
145
|
+
account = token_event(1000, 10, 20)
|
|
146
|
+
account["payload"]["info"]["rate_limits"]["limit_id"] = "codex"
|
|
147
|
+
other = token_event(2000, 90, 90)
|
|
148
|
+
other["payload"]["info"]["rate_limits"]["limit_id"] = "codex_other"
|
|
149
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
150
|
+
path = Path(tmp) / "s.jsonl"
|
|
151
|
+
path.write_bytes(line(account) + line(other))
|
|
152
|
+
quotas = LogReader().update(str(path))["quotas"]
|
|
153
|
+
self.assertEqual(quotas["5h"]["remaining"], 90.0)
|
|
154
|
+
self.assertEqual(quotas["weekly"]["observed_at"], 1000.0)
|
|
155
|
+
|
|
135
156
|
def test_newest_quota_across_sessions_wins(self):
|
|
136
157
|
with tempfile.TemporaryDirectory() as tmp:
|
|
137
158
|
day = Path(tmp) / "2026" / "09" / "30"
|
|
@@ -72,13 +72,3 @@ class FishTests(unittest.TestCase):
|
|
|
72
72
|
manage.installation(args, True)
|
|
73
73
|
self.assertEqual(fish.read_text(), "# later user setting\n")
|
|
74
74
|
self.assertNotIn(manage.BEGIN, (root / ".zshrc").read_text())
|
|
75
|
-
|
|
76
|
-
def test_shell_detection_prefers_parent_to_login_shell(self):
|
|
77
|
-
with (
|
|
78
|
-
patch.dict(os.environ, {"SHELL": "/bin/zsh"}),
|
|
79
|
-
patch(
|
|
80
|
-
"subprocess.run",
|
|
81
|
-
return_value=subprocess.CompletedProcess([], 0, "/opt/homebrew/bin/fish\n", ""),
|
|
82
|
-
),
|
|
83
|
-
):
|
|
84
|
-
self.assertEqual(manage.detected_shell(), "fish")
|
|
@@ -217,6 +217,24 @@ class RenderTests(unittest.TestCase):
|
|
|
217
217
|
self.assertNotIn("CTX", result)
|
|
218
218
|
self.assertIn("colour196", render(self.state, 240, segments=["5h"], crit_at=80))
|
|
219
219
|
|
|
220
|
+
def test_model_segment_is_optional_safe_and_width_bounded(self):
|
|
221
|
+
segments = ["model", "ctx", "tokens"]
|
|
222
|
+
plain = render(self.state, 240, tmux=False, segments=segments)
|
|
223
|
+
self.assertTrue(plain.startswith("Claude Sonnet 4.5 | CTX USED"), plain)
|
|
224
|
+
self.assertNotIn("Claude", render(self.state, 240, tmux=False))
|
|
225
|
+
hostile = dict(self.state, model="Opus#(touch NEVER)\x1b]0;title\x07\u202e長")
|
|
226
|
+
for width in range(1, 241):
|
|
227
|
+
for tmux, ansi in ((True, False), (False, True), (False, False)):
|
|
228
|
+
out = render(hostile, width, segments=segments, tmux=tmux, ansi=ansi)
|
|
229
|
+
self.assertLessEqual(visible_width(out), width)
|
|
230
|
+
self.assertNotIn("title", out)
|
|
231
|
+
self.assertNotIn("\u202e", out)
|
|
232
|
+
if tmux:
|
|
233
|
+
self.assertIsNone(re.search(r"(?<!#)#\(", out), out)
|
|
234
|
+
for missing in (None, 123, "", "\x1b[31m\u202e"):
|
|
235
|
+
out = render({"model": missing}, 80, tmux=False, segments=["model"])
|
|
236
|
+
self.assertIn("CTX USED", out) # no model metadata yet: retain a useful bar
|
|
237
|
+
|
|
220
238
|
|
|
221
239
|
class ConfigTests(unittest.TestCase):
|
|
222
240
|
def test_missing_file_defaults_and_invalid_keys_ignored(self):
|
|
@@ -227,6 +245,9 @@ class ConfigTests(unittest.TestCase):
|
|
|
227
245
|
):
|
|
228
246
|
path = Path(tmp) / "config.toml"
|
|
229
247
|
self.assertEqual(load_config(path)["segments"], ["ctx", "5h", "week", "tokens"])
|
|
248
|
+
self.assertEqual(
|
|
249
|
+
load_config(path, "claude")["segments"], ["model", "ctx", "5h", "week"]
|
|
250
|
+
)
|
|
230
251
|
path.write_text('segments = ["week", "ctx"]\ntheme = "neon"\nwarn_at = 30\n')
|
|
231
252
|
cfg = load_config(path)
|
|
232
253
|
self.assertEqual(
|
|
File without changes
|
|
File without changes
|
{codex_statusline-0.7.0 → codex_statusline-0.7.2}/codex_statusline.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{codex_statusline-0.7.0 → codex_statusline-0.7.2}/codex_statusline.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|