codex-statusline 0.1.3__tar.gz → 0.2.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.1.3 → codex_statusline-0.2.2}/PKG-INFO +20 -5
- {codex_statusline-0.1.3 → codex_statusline-0.2.2}/README.md +19 -4
- {codex_statusline-0.1.3 → codex_statusline-0.2.2}/codex_statusline.egg-info/PKG-INFO +20 -5
- {codex_statusline-0.1.3 → codex_statusline-0.2.2}/codex_statusline.egg-info/SOURCES.txt +2 -0
- {codex_statusline-0.1.3 → codex_statusline-0.2.2}/pyproject.toml +1 -1
- {codex_statusline-0.1.3 → codex_statusline-0.2.2}/src/__init__.py +1 -1
- {codex_statusline-0.1.3 → codex_statusline-0.2.2}/src/cli.py +24 -45
- codex_statusline-0.2.2/src/configure.py +174 -0
- {codex_statusline-0.1.3 → codex_statusline-0.2.2}/src/launcher.py +1 -1
- {codex_statusline-0.1.3 → codex_statusline-0.2.2}/src/manage.py +78 -42
- {codex_statusline-0.1.3 → codex_statusline-0.2.2}/src/render.py +7 -3
- {codex_statusline-0.1.3 → codex_statusline-0.2.2}/src/update.py +5 -3
- codex_statusline-0.2.2/tests/test_configure.py +42 -0
- {codex_statusline-0.1.3 → codex_statusline-0.2.2}/tests/test_management.py +15 -0
- {codex_statusline-0.1.3 → codex_statusline-0.2.2}/tests/test_startup.py +2 -0
- {codex_statusline-0.1.3 → codex_statusline-0.2.2}/tests/test_update.py +8 -6
- {codex_statusline-0.1.3 → codex_statusline-0.2.2}/LICENSE +0 -0
- {codex_statusline-0.1.3 → codex_statusline-0.2.2}/codex_statusline.egg-info/dependency_links.txt +0 -0
- {codex_statusline-0.1.3 → codex_statusline-0.2.2}/codex_statusline.egg-info/entry_points.txt +0 -0
- {codex_statusline-0.1.3 → codex_statusline-0.2.2}/codex_statusline.egg-info/top_level.txt +0 -0
- {codex_statusline-0.1.3 → codex_statusline-0.2.2}/setup.cfg +0 -0
- {codex_statusline-0.1.3 → codex_statusline-0.2.2}/src/__main__.py +0 -0
- {codex_statusline-0.1.3 → codex_statusline-0.2.2}/src/data.py +0 -0
- {codex_statusline-0.1.3 → codex_statusline-0.2.2}/src/entry.py +0 -0
- {codex_statusline-0.1.3 → codex_statusline-0.2.2}/src/hook.py +0 -0
- {codex_statusline-0.1.3 → codex_statusline-0.2.2}/tests/test_auto_shells.py +0 -0
- {codex_statusline-0.1.3 → codex_statusline-0.2.2}/tests/test_data.py +0 -0
- {codex_statusline-0.1.3 → codex_statusline-0.2.2}/tests/test_fish.py +0 -0
- {codex_statusline-0.1.3 → codex_statusline-0.2.2}/tests/test_render.py +0 -0
- {codex_statusline-0.1.3 → codex_statusline-0.2.2}/tests/test_tmux.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codex-statusline
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: A tmux statusline for OpenAI Codex CLI: context usage, 5h and weekly quota, and session tokens at a glance. Local-only.
|
|
5
5
|
Author: Zimo Xu
|
|
6
6
|
License-Expression: MIT
|
|
@@ -59,7 +59,7 @@ Prefer to do it yourself:
|
|
|
59
59
|
|
|
60
60
|
```sh
|
|
61
61
|
uv tool install codex-statusline # or: pipx install codex-statusline
|
|
62
|
-
codex-statusline install # add --dry-run to
|
|
62
|
+
codex-statusline install # add --dry-run to see the plan first
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
Then **open a new terminal** and run `codex`. The first time, Codex asks you to review a hook: confirm it is `codex-statusline binding`.
|
|
@@ -73,9 +73,9 @@ codex # same as always, now with a status bar
|
|
|
73
73
|
codex resume --last
|
|
74
74
|
codex exec 'task' # non-interactive: passed straight through, no bar
|
|
75
75
|
|
|
76
|
+
codex-statusline config # pick segments, theme and colors interactively
|
|
76
77
|
codex-statusline update # upgrade, whichever way you installed it
|
|
77
78
|
codex-statusline doctor # check versions and dependencies
|
|
78
|
-
codex-statusline preview --width 80 # try it without launching Codex
|
|
79
79
|
codex-statusline uninstall
|
|
80
80
|
```
|
|
81
81
|
|
|
@@ -83,7 +83,22 @@ Uninstall removes only what this tool added and still matches exactly. If you ed
|
|
|
83
83
|
|
|
84
84
|
## Configuration
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
Run `codex-statusline config` to set everything up in an interactive screen with a live preview: toggle and reorder segments, switch theme, set the warning thresholds, then press `s` to save.
|
|
87
|
+
|
|
88
|
+
```text
|
|
89
|
+
[x] ctx context used
|
|
90
|
+
[x] 5h 5-hour quota
|
|
91
|
+
[x] week weekly quota
|
|
92
|
+
[x] tokens session tokens
|
|
93
|
+
> Theme < auto >
|
|
94
|
+
Quota yellow at < 20% >
|
|
95
|
+
Quota red at < 5% >
|
|
96
|
+
|
|
97
|
+
Preview (sample numbers):
|
|
98
|
+
CTX USED ███░░░░░ 35% | 5h ██░░░░░░░░ 18% 3:46pm | week ██████░░░░ 62% Sat 1:46pm | tok 1.2M
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
It writes `~/.config/codex-statusline/config.toml`, which you can also edit by hand:
|
|
87
102
|
|
|
88
103
|
```toml
|
|
89
104
|
segments = ["ctx", "5h", "week", "tokens"] # pick and reorder
|
|
@@ -102,7 +117,7 @@ Invalid values are reported and fall back to defaults.
|
|
|
102
117
|
|
|
103
118
|
## FAQ
|
|
104
119
|
|
|
105
|
-
**How do I scroll or copy?**
|
|
120
|
+
**How do I scroll or copy?** Exactly as in plain Codex: the mouse wheel scrolls the conversation, and text selection works the same way.
|
|
106
121
|
|
|
107
122
|
**Windows?** Not supported (tmux).
|
|
108
123
|
|
|
@@ -38,7 +38,7 @@ Prefer to do it yourself:
|
|
|
38
38
|
|
|
39
39
|
```sh
|
|
40
40
|
uv tool install codex-statusline # or: pipx install codex-statusline
|
|
41
|
-
codex-statusline install # add --dry-run to
|
|
41
|
+
codex-statusline install # add --dry-run to see the plan first
|
|
42
42
|
```
|
|
43
43
|
|
|
44
44
|
Then **open a new terminal** and run `codex`. The first time, Codex asks you to review a hook: confirm it is `codex-statusline binding`.
|
|
@@ -52,9 +52,9 @@ codex # same as always, now with a status bar
|
|
|
52
52
|
codex resume --last
|
|
53
53
|
codex exec 'task' # non-interactive: passed straight through, no bar
|
|
54
54
|
|
|
55
|
+
codex-statusline config # pick segments, theme and colors interactively
|
|
55
56
|
codex-statusline update # upgrade, whichever way you installed it
|
|
56
57
|
codex-statusline doctor # check versions and dependencies
|
|
57
|
-
codex-statusline preview --width 80 # try it without launching Codex
|
|
58
58
|
codex-statusline uninstall
|
|
59
59
|
```
|
|
60
60
|
|
|
@@ -62,7 +62,22 @@ Uninstall removes only what this tool added and still matches exactly. If you ed
|
|
|
62
62
|
|
|
63
63
|
## Configuration
|
|
64
64
|
|
|
65
|
-
|
|
65
|
+
Run `codex-statusline config` to set everything up in an interactive screen with a live preview: toggle and reorder segments, switch theme, set the warning thresholds, then press `s` to save.
|
|
66
|
+
|
|
67
|
+
```text
|
|
68
|
+
[x] ctx context used
|
|
69
|
+
[x] 5h 5-hour quota
|
|
70
|
+
[x] week weekly quota
|
|
71
|
+
[x] tokens session tokens
|
|
72
|
+
> Theme < auto >
|
|
73
|
+
Quota yellow at < 20% >
|
|
74
|
+
Quota red at < 5% >
|
|
75
|
+
|
|
76
|
+
Preview (sample numbers):
|
|
77
|
+
CTX USED ███░░░░░ 35% | 5h ██░░░░░░░░ 18% 3:46pm | week ██████░░░░ 62% Sat 1:46pm | tok 1.2M
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
It writes `~/.config/codex-statusline/config.toml`, which you can also edit by hand:
|
|
66
81
|
|
|
67
82
|
```toml
|
|
68
83
|
segments = ["ctx", "5h", "week", "tokens"] # pick and reorder
|
|
@@ -81,7 +96,7 @@ Invalid values are reported and fall back to defaults.
|
|
|
81
96
|
|
|
82
97
|
## FAQ
|
|
83
98
|
|
|
84
|
-
**How do I scroll or copy?**
|
|
99
|
+
**How do I scroll or copy?** Exactly as in plain Codex: the mouse wheel scrolls the conversation, and text selection works the same way.
|
|
85
100
|
|
|
86
101
|
**Windows?** Not supported (tmux).
|
|
87
102
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codex-statusline
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: A tmux statusline for OpenAI Codex CLI: context usage, 5h and weekly quota, and session tokens at a glance. Local-only.
|
|
5
5
|
Author: Zimo Xu
|
|
6
6
|
License-Expression: MIT
|
|
@@ -59,7 +59,7 @@ Prefer to do it yourself:
|
|
|
59
59
|
|
|
60
60
|
```sh
|
|
61
61
|
uv tool install codex-statusline # or: pipx install codex-statusline
|
|
62
|
-
codex-statusline install # add --dry-run to
|
|
62
|
+
codex-statusline install # add --dry-run to see the plan first
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
Then **open a new terminal** and run `codex`. The first time, Codex asks you to review a hook: confirm it is `codex-statusline binding`.
|
|
@@ -73,9 +73,9 @@ codex # same as always, now with a status bar
|
|
|
73
73
|
codex resume --last
|
|
74
74
|
codex exec 'task' # non-interactive: passed straight through, no bar
|
|
75
75
|
|
|
76
|
+
codex-statusline config # pick segments, theme and colors interactively
|
|
76
77
|
codex-statusline update # upgrade, whichever way you installed it
|
|
77
78
|
codex-statusline doctor # check versions and dependencies
|
|
78
|
-
codex-statusline preview --width 80 # try it without launching Codex
|
|
79
79
|
codex-statusline uninstall
|
|
80
80
|
```
|
|
81
81
|
|
|
@@ -83,7 +83,22 @@ Uninstall removes only what this tool added and still matches exactly. If you ed
|
|
|
83
83
|
|
|
84
84
|
## Configuration
|
|
85
85
|
|
|
86
|
-
|
|
86
|
+
Run `codex-statusline config` to set everything up in an interactive screen with a live preview: toggle and reorder segments, switch theme, set the warning thresholds, then press `s` to save.
|
|
87
|
+
|
|
88
|
+
```text
|
|
89
|
+
[x] ctx context used
|
|
90
|
+
[x] 5h 5-hour quota
|
|
91
|
+
[x] week weekly quota
|
|
92
|
+
[x] tokens session tokens
|
|
93
|
+
> Theme < auto >
|
|
94
|
+
Quota yellow at < 20% >
|
|
95
|
+
Quota red at < 5% >
|
|
96
|
+
|
|
97
|
+
Preview (sample numbers):
|
|
98
|
+
CTX USED ███░░░░░ 35% | 5h ██░░░░░░░░ 18% 3:46pm | week ██████░░░░ 62% Sat 1:46pm | tok 1.2M
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
It writes `~/.config/codex-statusline/config.toml`, which you can also edit by hand:
|
|
87
102
|
|
|
88
103
|
```toml
|
|
89
104
|
segments = ["ctx", "5h", "week", "tokens"] # pick and reorder
|
|
@@ -102,7 +117,7 @@ Invalid values are reported and fall back to defaults.
|
|
|
102
117
|
|
|
103
118
|
## FAQ
|
|
104
119
|
|
|
105
|
-
**How do I scroll or copy?**
|
|
120
|
+
**How do I scroll or copy?** Exactly as in plain Codex: the mouse wheel scrolls the conversation, and text selection works the same way.
|
|
106
121
|
|
|
107
122
|
**Windows?** Not supported (tmux).
|
|
108
123
|
|
|
@@ -9,6 +9,7 @@ codex_statusline.egg-info/top_level.txt
|
|
|
9
9
|
src/__init__.py
|
|
10
10
|
src/__main__.py
|
|
11
11
|
src/cli.py
|
|
12
|
+
src/configure.py
|
|
12
13
|
src/data.py
|
|
13
14
|
src/entry.py
|
|
14
15
|
src/hook.py
|
|
@@ -17,6 +18,7 @@ src/manage.py
|
|
|
17
18
|
src/render.py
|
|
18
19
|
src/update.py
|
|
19
20
|
tests/test_auto_shells.py
|
|
21
|
+
tests/test_configure.py
|
|
20
22
|
tests/test_data.py
|
|
21
23
|
tests/test_fish.py
|
|
22
24
|
tests/test_management.py
|
|
@@ -3,7 +3,6 @@ import json
|
|
|
3
3
|
import os
|
|
4
4
|
import shutil
|
|
5
5
|
import sys
|
|
6
|
-
import time
|
|
7
6
|
from pathlib import Path
|
|
8
7
|
|
|
9
8
|
from . import __version__
|
|
@@ -26,38 +25,44 @@ def main():
|
|
|
26
25
|
record()
|
|
27
26
|
return 0
|
|
28
27
|
parser = argparse.ArgumentParser(
|
|
29
|
-
|
|
28
|
+
prog="codex-statusline",
|
|
29
|
+
description="A status bar for Codex CLI: context, 5h/weekly quota, and tokens. "
|
|
30
|
+
"Run `codex` as usual once installed.",
|
|
30
31
|
)
|
|
31
32
|
parser.add_argument("--version", action="version", version=__version__)
|
|
32
|
-
sub = parser.add_subparsers(dest="action", required=True)
|
|
33
|
+
sub = parser.add_subparsers(dest="action", required=True, metavar="<command>", title="commands")
|
|
34
|
+
commands = {
|
|
35
|
+
"install": "hook the status bar into `codex` for your shells",
|
|
36
|
+
"uninstall": "remove everything install added",
|
|
37
|
+
"config": "choose what the bar shows, interactively",
|
|
38
|
+
"update": "upgrade to the latest release",
|
|
39
|
+
"doctor": "check Codex, tmux, and Python versions",
|
|
40
|
+
}
|
|
41
|
+
parsers = {
|
|
42
|
+
name: sub.add_parser(name, help=text, description=text) for name, text in commands.items()
|
|
43
|
+
}
|
|
33
44
|
for name in ("install", "uninstall"):
|
|
34
|
-
s =
|
|
45
|
+
s = parsers[name]
|
|
35
46
|
s.add_argument(
|
|
36
47
|
"--shell",
|
|
37
48
|
choices=["auto", "bash", "zsh", "fish", "both", "all"],
|
|
38
49
|
default="auto",
|
|
39
|
-
help="default:
|
|
50
|
+
help="default: every supported shell found on PATH",
|
|
40
51
|
)
|
|
41
|
-
s.add_argument("--home", default=str(Path.home()), help=
|
|
42
|
-
s.add_argument(
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
help="approve printed diff without interactive prompt",
|
|
46
|
-
)
|
|
47
|
-
s.add_argument("--dry-run", action="store_true")
|
|
48
|
-
sub.add_parser("doctor")
|
|
49
|
-
sub.add_parser("update", help="upgrade codex-statusline the way it was installed")
|
|
50
|
-
p = sub.add_parser("preview")
|
|
51
|
-
p.add_argument("--width", type=int, default=120)
|
|
52
|
-
p.add_argument("--theme", choices=["auto", "dark", "light"])
|
|
53
|
-
p.add_argument("--ascii", action="store_true")
|
|
54
|
-
p.add_argument("--tmux", action="store_true", help="emit tmux style syntax, not ANSI")
|
|
52
|
+
s.add_argument("--home", default=str(Path.home()), help=argparse.SUPPRESS) # tests
|
|
53
|
+
s.add_argument("--yes", action="store_true", help="apply without asking")
|
|
54
|
+
s.add_argument("--diff", action="store_true", help="show the exact file changes")
|
|
55
|
+
s.add_argument("--dry-run", action="store_true", help="show the plan, change nothing")
|
|
55
56
|
ns = parser.parse_args(args)
|
|
56
57
|
try:
|
|
57
58
|
if ns.action in ("install", "uninstall"):
|
|
58
59
|
from .manage import installation
|
|
59
60
|
|
|
60
61
|
return installation(ns, ns.action == "uninstall")
|
|
62
|
+
if ns.action == "config":
|
|
63
|
+
from .configure import configure
|
|
64
|
+
|
|
65
|
+
return configure()
|
|
61
66
|
if ns.action == "update":
|
|
62
67
|
from .update import update
|
|
63
68
|
|
|
@@ -75,32 +80,6 @@ def main():
|
|
|
75
80
|
}
|
|
76
81
|
print(json.dumps(data, indent=2))
|
|
77
82
|
return 0 if data["tmux"] and version(path) >= (0, 159, 0) else 1
|
|
78
|
-
if ns.action == "preview":
|
|
79
|
-
from .render import load_config, render
|
|
80
|
-
|
|
81
|
-
cfg = load_config()
|
|
82
|
-
now = time.time()
|
|
83
|
-
state = {
|
|
84
|
-
"context_used": 35,
|
|
85
|
-
"tokens": 1_234_567,
|
|
86
|
-
"quotas": {
|
|
87
|
-
"5h": {"remaining": 78, "reset_at": now + 7200, "observed_at": now},
|
|
88
|
-
"weekly": {"remaining": 39, "reset_at": now + 172800, "observed_at": now},
|
|
89
|
-
},
|
|
90
|
-
}
|
|
91
|
-
print(
|
|
92
|
-
render(
|
|
93
|
-
state,
|
|
94
|
-
ns.width,
|
|
95
|
-
theme=ns.theme or cfg["theme"],
|
|
96
|
-
ascii_only=ns.ascii or cfg["ascii"],
|
|
97
|
-
tmux=ns.tmux,
|
|
98
|
-
segments=cfg["segments"],
|
|
99
|
-
warn_at=cfg["warn_at"],
|
|
100
|
-
crit_at=cfg["crit_at"],
|
|
101
|
-
)
|
|
102
|
-
)
|
|
103
|
-
return 0
|
|
104
83
|
except (OSError, ValueError, RuntimeError, KeyError) as error:
|
|
105
84
|
print(f"codex-statusline: {error}", file=sys.stderr)
|
|
106
85
|
return 1
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""`codex-statusline config`: pick segments, theme and thresholds with a live preview."""
|
|
2
|
+
|
|
3
|
+
import curses
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
from .render import DEFAULT_SEGMENTS, THEMES, config_path, load_config, render
|
|
7
|
+
|
|
8
|
+
SEGMENT_NAMES = {
|
|
9
|
+
"ctx": "context used",
|
|
10
|
+
"5h": "5-hour quota",
|
|
11
|
+
"week": "weekly quota",
|
|
12
|
+
"tokens": "session tokens",
|
|
13
|
+
}
|
|
14
|
+
SETTINGS = ["theme", "ascii", "warn_at", "crit_at", "update_check"]
|
|
15
|
+
LABELS = {
|
|
16
|
+
"theme": "Theme",
|
|
17
|
+
"ascii": "ASCII only",
|
|
18
|
+
"warn_at": "Quota yellow at",
|
|
19
|
+
"crit_at": "Quota red at",
|
|
20
|
+
"update_check": "Update notice",
|
|
21
|
+
}
|
|
22
|
+
HELP = "↑↓ move space toggle ←→ change J/K reorder s save q quit"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def model_from(cfg: dict) -> dict:
|
|
26
|
+
enabled = [name for name in cfg["segments"] if name in SEGMENT_NAMES]
|
|
27
|
+
order = enabled + [name for name in DEFAULT_SEGMENTS if name not in enabled]
|
|
28
|
+
return {**{key: cfg[key] for key in SETTINGS}, "order": order, "enabled": set(enabled)}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def to_config(model: dict) -> dict:
|
|
32
|
+
segments = [name for name in model["order"] if name in model["enabled"]]
|
|
33
|
+
return {"segments": segments, **{key: model[key] for key in SETTINGS}}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def dump_toml(cfg: dict) -> str:
|
|
37
|
+
def value(v):
|
|
38
|
+
if isinstance(v, bool):
|
|
39
|
+
return "true" if v else "false"
|
|
40
|
+
if isinstance(v, list):
|
|
41
|
+
return "[" + ", ".join(f'"{x}"' for x in v) + "]"
|
|
42
|
+
return f'"{v}"' if isinstance(v, str) else str(v)
|
|
43
|
+
|
|
44
|
+
return "".join(f"{key} = {value(v)}\n" for key, v in cfg.items())
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def row_count(model: dict) -> int:
|
|
48
|
+
return len(model["order"]) + len(SETTINGS)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def apply(model: dict, row: int, key: str) -> int:
|
|
52
|
+
"""Apply one key press to the row under the cursor; returns the new cursor row."""
|
|
53
|
+
rows = row_count(model)
|
|
54
|
+
if key in ("up", "k"):
|
|
55
|
+
return (row - 1) % rows
|
|
56
|
+
if key in ("down", "j"):
|
|
57
|
+
return (row + 1) % rows
|
|
58
|
+
order = model["order"]
|
|
59
|
+
if row < len(order):
|
|
60
|
+
name = order[row]
|
|
61
|
+
if key == "space":
|
|
62
|
+
# Keep at least one segment on, or the bar would be empty.
|
|
63
|
+
if name not in model["enabled"]:
|
|
64
|
+
model["enabled"].add(name)
|
|
65
|
+
elif len(model["enabled"]) > 1:
|
|
66
|
+
model["enabled"].discard(name)
|
|
67
|
+
elif key in ("K", "J"):
|
|
68
|
+
target = row - 1 if key == "K" else row + 1
|
|
69
|
+
if 0 <= target < len(order):
|
|
70
|
+
order[row], order[target] = order[target], order[row]
|
|
71
|
+
return target
|
|
72
|
+
return row
|
|
73
|
+
setting = SETTINGS[row - len(order)]
|
|
74
|
+
current = model[setting]
|
|
75
|
+
step = {"left": -1, "right": 1}.get(key, 0)
|
|
76
|
+
if isinstance(current, bool):
|
|
77
|
+
if key in ("space", "left", "right"):
|
|
78
|
+
model[setting] = not current
|
|
79
|
+
elif setting == "theme" and (step or key == "space"):
|
|
80
|
+
names = list(THEMES)
|
|
81
|
+
model[setting] = names[(names.index(current) + (step or 1)) % len(names)]
|
|
82
|
+
elif step:
|
|
83
|
+
model[setting] = max(0, min(100, current + 5 * step))
|
|
84
|
+
return row
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def rows_text(model: dict) -> list[str]:
|
|
88
|
+
lines = []
|
|
89
|
+
for name in model["order"]:
|
|
90
|
+
mark = "x" if name in model["enabled"] else " "
|
|
91
|
+
lines.append(f"[{mark}] {name:<7} {SEGMENT_NAMES[name]}")
|
|
92
|
+
for setting in SETTINGS:
|
|
93
|
+
value = model[setting]
|
|
94
|
+
if isinstance(value, bool):
|
|
95
|
+
shown = "[x]" if value else "[ ]"
|
|
96
|
+
elif setting == "theme":
|
|
97
|
+
shown = f"< {value} >"
|
|
98
|
+
else:
|
|
99
|
+
shown = f"< {value}% >"
|
|
100
|
+
lines.append(f"{LABELS[setting]:<16} {shown}")
|
|
101
|
+
return lines
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def sample_state() -> dict:
|
|
105
|
+
now = time.time()
|
|
106
|
+
return {
|
|
107
|
+
"context_used": 35,
|
|
108
|
+
"tokens": 1_234_567,
|
|
109
|
+
"quotas": {
|
|
110
|
+
"5h": {"remaining": 18, "reset_at": now + 7200, "observed_at": now},
|
|
111
|
+
"weekly": {"remaining": 62, "reset_at": now + 172800, "observed_at": now},
|
|
112
|
+
},
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
KEYS = {
|
|
117
|
+
curses.KEY_UP: "up",
|
|
118
|
+
curses.KEY_DOWN: "down",
|
|
119
|
+
curses.KEY_LEFT: "left",
|
|
120
|
+
curses.KEY_RIGHT: "right",
|
|
121
|
+
ord(" "): "space",
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _ui(screen, model: dict) -> bool:
|
|
126
|
+
curses.curs_set(0)
|
|
127
|
+
row, state = 0, sample_state()
|
|
128
|
+
while True:
|
|
129
|
+
screen.erase()
|
|
130
|
+
height, width = screen.getmaxyx()
|
|
131
|
+
cfg = to_config(model)
|
|
132
|
+
preview = render(
|
|
133
|
+
state,
|
|
134
|
+
max(1, width - 4),
|
|
135
|
+
tmux=False,
|
|
136
|
+
ascii_only=cfg["ascii"],
|
|
137
|
+
segments=cfg["segments"],
|
|
138
|
+
warn_at=cfg["warn_at"],
|
|
139
|
+
crit_at=cfg["crit_at"],
|
|
140
|
+
)
|
|
141
|
+
lines = ["codex-statusline config", HELP, ""]
|
|
142
|
+
lines += [("> " if i == row else " ") + text for i, text in enumerate(rows_text(model))]
|
|
143
|
+
lines += ["", "Preview (sample numbers):", " " + preview, "", f"Saves to {config_path()}"]
|
|
144
|
+
for y, text in enumerate(lines[: height - 1]):
|
|
145
|
+
highlight = curses.A_REVERSE if y - 3 == row else curses.A_NORMAL
|
|
146
|
+
try:
|
|
147
|
+
screen.addnstr(y, 0, text, width - 1, highlight)
|
|
148
|
+
except curses.error:
|
|
149
|
+
pass # tiny terminal: draw what fits
|
|
150
|
+
key = screen.getch()
|
|
151
|
+
name = KEYS.get(key, chr(key) if 0 <= key < 256 else "")
|
|
152
|
+
if name in ("q", "\x1b"):
|
|
153
|
+
return False
|
|
154
|
+
if name in ("s", "\n"):
|
|
155
|
+
return True
|
|
156
|
+
row = apply(model, row, name)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def configure() -> int:
|
|
160
|
+
model = model_from(load_config())
|
|
161
|
+
try:
|
|
162
|
+
save = curses.wrapper(_ui, model)
|
|
163
|
+
except curses.error as error:
|
|
164
|
+
print(f"codex-statusline: needs an interactive terminal ({error}).")
|
|
165
|
+
print(f"Edit {config_path()} instead.")
|
|
166
|
+
return 1
|
|
167
|
+
if not save:
|
|
168
|
+
print("No changes saved.")
|
|
169
|
+
return 0
|
|
170
|
+
path = config_path()
|
|
171
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
172
|
+
path.write_text(dump_toml(to_config(model)))
|
|
173
|
+
print(f"Saved {path}. Takes effect the next time you start codex.")
|
|
174
|
+
return 0
|
|
@@ -215,7 +215,7 @@ def launch(args):
|
|
|
215
215
|
'set -g status-left ""\n'
|
|
216
216
|
"set -g allow-rename off\n"
|
|
217
217
|
"set -g set-titles off\n"
|
|
218
|
-
"set -g mouse
|
|
218
|
+
"set -g mouse on\n"
|
|
219
219
|
"set -g history-limit 10000\n"
|
|
220
220
|
"set -g remain-on-exit on\n"
|
|
221
221
|
"set -g exit-empty on\n"
|
|
@@ -16,6 +16,7 @@ from .launcher import ENTRY
|
|
|
16
16
|
|
|
17
17
|
BEGIN = "# >>> codex-statusline >>>"
|
|
18
18
|
END = "# <<< codex-statusline <<<"
|
|
19
|
+
STATUS_MESSAGE = "codex-statusline binding"
|
|
19
20
|
|
|
20
21
|
|
|
21
22
|
def detected_shell():
|
|
@@ -96,7 +97,7 @@ def hook_group():
|
|
|
96
97
|
"type": "command",
|
|
97
98
|
"command": shlex.join([*base_command(), "hook"]),
|
|
98
99
|
"timeout": 2,
|
|
99
|
-
"statusMessage":
|
|
100
|
+
"statusMessage": STATUS_MESSAGE,
|
|
100
101
|
}
|
|
101
102
|
]
|
|
102
103
|
}
|
|
@@ -167,14 +168,45 @@ def modify_hook(raw, group, add):
|
|
|
167
168
|
return json.dumps(obj, ensure_ascii=False, indent=2) + "\n"
|
|
168
169
|
|
|
169
170
|
|
|
171
|
+
def summary(changed, home):
|
|
172
|
+
"""One line per file: what is added or removed, in words rather than a diff."""
|
|
173
|
+
lines = ["codex-statusline will:"]
|
|
174
|
+
for p, old, new in changed:
|
|
175
|
+
what = "the SessionStart hook" if p.name == "hooks.json" else "the codex shell function"
|
|
176
|
+
if p.name == "hooks.json": # JSON is re-serialized, so compare our marker instead
|
|
177
|
+
had, has = (STATUS_MESSAGE in t for t in (old, new))
|
|
178
|
+
sign, verb = (
|
|
179
|
+
("+", "add") if has and not had else ("-", "remove") if had else ("~", "update")
|
|
180
|
+
)
|
|
181
|
+
elif old in new:
|
|
182
|
+
sign, verb = "+", "add"
|
|
183
|
+
elif new in old:
|
|
184
|
+
sign, verb = "-", "remove"
|
|
185
|
+
else:
|
|
186
|
+
sign, verb = "~", "update"
|
|
187
|
+
lines.append(f" {sign} {tilde(p, home):<44} {verb} {what}")
|
|
188
|
+
return "\n".join(lines)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def tilde(p, home):
|
|
192
|
+
try:
|
|
193
|
+
return "~/" + str(Path(p).relative_to(home))
|
|
194
|
+
except ValueError:
|
|
195
|
+
return str(p)
|
|
196
|
+
|
|
197
|
+
|
|
170
198
|
def installation(args, uninstall=False):
|
|
171
199
|
chome, shells, manifest_path = paths(args.home, args.shell)
|
|
172
|
-
changes = []
|
|
173
200
|
previous = json.loads(text(manifest_path) or "{}")
|
|
174
201
|
if uninstall and not previous:
|
|
175
202
|
print("Nothing installed by codex-statusline.")
|
|
176
203
|
return 0
|
|
177
|
-
|
|
204
|
+
pending = {} # path -> new text; several steps may edit the same file
|
|
205
|
+
|
|
206
|
+
def current(p):
|
|
207
|
+
return pending.get(p, text(p))
|
|
208
|
+
|
|
209
|
+
def remove_previous():
|
|
178
210
|
# Manifest defines owned paths; never use current shell choice to guess.
|
|
179
211
|
expected_home, allowed_shells, _ = paths(args.home, "all")
|
|
180
212
|
if previous.get("hooks_path") != str(expected_home / "hooks.json") or any(
|
|
@@ -183,19 +215,20 @@ def installation(args, uninstall=False):
|
|
|
183
215
|
or not i["block"].endswith(END + "\n")
|
|
184
216
|
for i in previous.get("shells", [])
|
|
185
217
|
):
|
|
186
|
-
raise ValueError("Manifest paths/blocks do not match this home; refusing
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
old = text(hp)
|
|
190
|
-
new = modify_hook(old, group, False)
|
|
191
|
-
changes.append((hp, old, new))
|
|
218
|
+
raise ValueError("Manifest paths/blocks do not match this home; refusing to remove it.")
|
|
219
|
+
php = Path(previous["hooks_path"])
|
|
220
|
+
pending[php] = modify_hook(current(php), previous["hook_group"], False)
|
|
192
221
|
for item in previous["shells"]:
|
|
193
222
|
p = Path(item["path"])
|
|
194
|
-
old =
|
|
195
|
-
|
|
196
|
-
if owned not in old and BEGIN in old:
|
|
223
|
+
old = current(p)
|
|
224
|
+
if item["block"] not in old and BEGIN in old:
|
|
197
225
|
raise ValueError(f"Owned shell block was edited; remove it manually: {p}")
|
|
198
|
-
|
|
226
|
+
pending[p] = old.replace(item["block"], "", 1)
|
|
227
|
+
|
|
228
|
+
migrating = False
|
|
229
|
+
if uninstall:
|
|
230
|
+
group, hp = previous["hook_group"], Path(previous["hooks_path"])
|
|
231
|
+
remove_previous()
|
|
199
232
|
else:
|
|
200
233
|
cfg = tomllib.loads(text(chome / "config.toml"))
|
|
201
234
|
hook_config = cfg.get("hooks", {})
|
|
@@ -206,40 +239,37 @@ def installation(args, uninstall=False):
|
|
|
206
239
|
)
|
|
207
240
|
group = hook_group()
|
|
208
241
|
hp = chome / "hooks.json"
|
|
209
|
-
|
|
210
|
-
|
|
242
|
+
# An older install from another path (e.g. a git checkout before uv/pipx):
|
|
243
|
+
# swap it out in the same reviewed diff instead of asking for an uninstall first.
|
|
244
|
+
migrating = bool(previous) and (
|
|
245
|
+
previous.get("hook_group") != group or previous.get("hooks_path") != str(hp)
|
|
246
|
+
)
|
|
247
|
+
if migrating:
|
|
248
|
+
print("Replacing a previous codex-statusline installation from another path.")
|
|
249
|
+
remove_previous()
|
|
250
|
+
pending[hp] = modify_hook(current(hp), group, True)
|
|
211
251
|
for p in shells:
|
|
212
252
|
block = shell_block("fish" if p.suffix == ".fish" else "zsh")
|
|
213
|
-
old =
|
|
253
|
+
old = current(p)
|
|
214
254
|
if BEGIN in old and block not in old:
|
|
215
255
|
raise ValueError(
|
|
216
256
|
f"A different/edited codex-statusline block exists in {p}; "
|
|
217
257
|
"uninstall that version first."
|
|
218
258
|
)
|
|
219
|
-
|
|
220
|
-
if previous and (
|
|
221
|
-
previous.get("hook_group") != group or previous.get("hooks_path") != str(hp)
|
|
222
|
-
):
|
|
223
|
-
raise ValueError(
|
|
224
|
-
"Existing installation uses another project/hook path. Uninstall it first."
|
|
225
|
-
)
|
|
259
|
+
pending[p] = old if block in old else old + block
|
|
226
260
|
allowed = paths(args.home, "all")[1]
|
|
227
261
|
if any(Path(x["path"]) not in allowed for x in previous.get("shells", [])):
|
|
228
262
|
raise ValueError("Existing manifest contains unexpected shell paths.")
|
|
263
|
+
changes = [(p, text(p), new) for p, new in pending.items()]
|
|
229
264
|
changed = [(p, old, new) for p, old, new in changes if old != new]
|
|
230
|
-
|
|
231
|
-
print(
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
n=0,
|
|
239
|
-
)
|
|
240
|
-
),
|
|
241
|
-
end="",
|
|
242
|
-
)
|
|
265
|
+
if changed:
|
|
266
|
+
print(summary(changed, Path(args.home).expanduser()))
|
|
267
|
+
if changed and (getattr(args, "diff", False) or args.dry_run):
|
|
268
|
+
for p, old, new in changed:
|
|
269
|
+
diff = difflib.unified_diff(
|
|
270
|
+
old.splitlines(True), new.splitlines(True), str(p), str(p), n=0
|
|
271
|
+
)
|
|
272
|
+
print("".join(diff), end="")
|
|
243
273
|
if args.dry_run:
|
|
244
274
|
print("Dry run: no files written.")
|
|
245
275
|
return 0
|
|
@@ -252,7 +282,12 @@ def installation(args, uninstall=False):
|
|
|
252
282
|
print("Already installed; no configuration changes.")
|
|
253
283
|
return 0
|
|
254
284
|
if not args.yes:
|
|
255
|
-
|
|
285
|
+
hint = "" if getattr(args, "diff", False) else " (--diff shows the exact changes)"
|
|
286
|
+
if not sys.stdin.isatty() or input(f"Apply?{hint} [Y/n] ").strip().lower() not in (
|
|
287
|
+
"",
|
|
288
|
+
"y",
|
|
289
|
+
"yes",
|
|
290
|
+
):
|
|
256
291
|
print("No files changed.")
|
|
257
292
|
return 0
|
|
258
293
|
# Refuse symlinks for edited files. User must explicitly manage those targets.
|
|
@@ -284,7 +319,8 @@ def installation(args, uninstall=False):
|
|
|
284
319
|
if uninstall:
|
|
285
320
|
manifest_path.unlink(missing_ok=True)
|
|
286
321
|
else:
|
|
287
|
-
|
|
322
|
+
kept = [] if migrating else previous.get("shells", [])
|
|
323
|
+
owned = {item["path"]: item for item in kept}
|
|
288
324
|
for p in shells:
|
|
289
325
|
owned[str(p)] = {
|
|
290
326
|
"path": str(p),
|
|
@@ -306,10 +342,10 @@ def installation(args, uninstall=False):
|
|
|
306
342
|
p.unlink(missing_ok=True)
|
|
307
343
|
raise
|
|
308
344
|
print(
|
|
309
|
-
"Uninstalled. Open a
|
|
345
|
+
"Uninstalled. Open a new terminal to finish."
|
|
310
346
|
if uninstall
|
|
311
|
-
else "Installed. Open a
|
|
312
|
-
"
|
|
347
|
+
else "Installed. Open a new terminal and run codex; trust the "
|
|
348
|
+
f"'{STATUS_MESSAGE}' hook when Codex asks."
|
|
313
349
|
)
|
|
314
|
-
print(f"
|
|
350
|
+
print(f"Backup: {tilde(backup, Path(args.home).expanduser())}")
|
|
315
351
|
return 0
|
|
@@ -35,11 +35,15 @@ _CONFIG_DEFAULTS = {
|
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
|
|
38
|
+
def config_path() -> Path:
|
|
39
|
+
base = os.environ.get("XDG_CONFIG_HOME") or str(Path.home() / ".config")
|
|
40
|
+
return Path(base) / "codex-statusline" / "config.toml"
|
|
41
|
+
|
|
42
|
+
|
|
38
43
|
def load_config(path: str | os.PathLike | None = None) -> dict[str, Any]:
|
|
39
44
|
"""Read $XDG_CONFIG_HOME/codex-statusline/config.toml; invalid values fall back to defaults."""
|
|
40
45
|
if path is None:
|
|
41
|
-
|
|
42
|
-
path = Path(base) / "codex-statusline" / "config.toml"
|
|
46
|
+
path = config_path()
|
|
43
47
|
config = dict(_CONFIG_DEFAULTS)
|
|
44
48
|
try:
|
|
45
49
|
raw = tomllib.loads(Path(path).read_text())
|
|
@@ -274,7 +278,7 @@ def render(
|
|
|
274
278
|
hint = None
|
|
275
279
|
if isinstance(update, str) and re.fullmatch(r"\d+(\.\d+)*", update):
|
|
276
280
|
arrow = "^" if ascii_only else "↑"
|
|
277
|
-
hint = (f"{arrow}
|
|
281
|
+
hint = (f"{arrow} update available: codex-statusline update", f"{arrow} update", "warning")
|
|
278
282
|
head, rest = parts[0], parts[1:]
|
|
279
283
|
compact_head = (head[1], head[2])
|
|
280
284
|
# Widest first: all detail, compact head, all compact, then drop segments.
|
|
@@ -38,11 +38,13 @@ def newer_version(timeout: float = 2) -> str | None:
|
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
def upgrade_command(prefix: str = sys.prefix, package: Path = PACKAGE) -> list[str]:
|
|
41
|
-
# uv and pipx each leave a marker file in the tool's environment.
|
|
41
|
+
# uv and pipx each leave a marker file in the tool's environment. Install the latest
|
|
42
|
+
# release from PyPI explicitly: `upgrade` keeps the original source, so a tool first
|
|
43
|
+
# installed from a local checkout would otherwise never leave that version.
|
|
42
44
|
if (Path(prefix) / "uv-receipt.toml").exists():
|
|
43
|
-
return ["uv", "tool", "
|
|
45
|
+
return ["uv", "tool", "install", "codex-statusline@latest"]
|
|
44
46
|
if (Path(prefix) / "pipx_metadata.json").exists():
|
|
45
|
-
return ["pipx", "
|
|
47
|
+
return ["pipx", "install", "--force", "codex-statusline"]
|
|
46
48
|
if (package.parent / ".git").exists():
|
|
47
49
|
return ["git", "-C", str(package.parent), "pull", "--ff-only"]
|
|
48
50
|
return [sys.executable, "-m", "pip", "install", "--upgrade", "codex-statusline"]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import tempfile
|
|
2
|
+
import unittest
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from unittest.mock import patch
|
|
5
|
+
|
|
6
|
+
from src.configure import apply, dump_toml, model_from, rows_text, to_config
|
|
7
|
+
from src.render import load_config
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ConfigureTests(unittest.TestCase):
|
|
11
|
+
def setUp(self):
|
|
12
|
+
with patch.dict("os.environ", {}, clear=True):
|
|
13
|
+
self.model = model_from(load_config("/nonexistent/config.toml"))
|
|
14
|
+
|
|
15
|
+
def test_toggle_reorder_and_adjust(self):
|
|
16
|
+
m = self.model
|
|
17
|
+
row = apply(m, 3, "space") # turn tokens off
|
|
18
|
+
row = apply(m, 1, "J") # move 5h below week
|
|
19
|
+
self.assertEqual(row, 2)
|
|
20
|
+
apply(m, 4, "right") # theme auto -> dark
|
|
21
|
+
apply(m, 6, "left") # warn_at 20 -> 15
|
|
22
|
+
apply(m, 7, "left") # crit_at 5 -> 0
|
|
23
|
+
apply(m, 7, "left") # clamps at 0
|
|
24
|
+
apply(m, 8, "space") # update notice off
|
|
25
|
+
cfg = to_config(m)
|
|
26
|
+
self.assertEqual(cfg["segments"], ["ctx", "week", "5h"])
|
|
27
|
+
self.assertEqual((cfg["theme"], cfg["warn_at"], cfg["crit_at"]), ("dark", 15, 0))
|
|
28
|
+
self.assertFalse(cfg["update_check"])
|
|
29
|
+
self.assertIn("[ ] tokens", "\n".join(rows_text(m)))
|
|
30
|
+
|
|
31
|
+
def test_last_segment_cannot_be_turned_off(self):
|
|
32
|
+
for row in range(4):
|
|
33
|
+
apply(self.model, row, "space")
|
|
34
|
+
self.assertEqual(to_config(self.model)["segments"], ["tokens"])
|
|
35
|
+
|
|
36
|
+
def test_saved_file_loads_back(self):
|
|
37
|
+
apply(self.model, 0, "J")
|
|
38
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
39
|
+
path = Path(tmp) / "config.toml"
|
|
40
|
+
path.write_text(dump_toml(to_config(self.model)))
|
|
41
|
+
with patch.dict("os.environ", {}, clear=True):
|
|
42
|
+
self.assertEqual(load_config(path), to_config(self.model))
|
|
@@ -53,6 +53,21 @@ class InstallTests(unittest.TestCase):
|
|
|
53
53
|
str(ENTRY), manage.hook_group()["hooks"][0]["command"]
|
|
54
54
|
) # git-clone mode unchanged
|
|
55
55
|
|
|
56
|
+
def test_install_from_new_path_replaces_previous_install(self):
|
|
57
|
+
(self.root / ".zshrc").write_text("# mine\n")
|
|
58
|
+
manage.installation(self.args) # e.g. an old git-checkout install
|
|
59
|
+
tool = ["/home/u/.local/bin/codex-statusline"]
|
|
60
|
+
with patch.object(manage, "base_command", return_value=tool):
|
|
61
|
+
manage.installation(self.args) # now from uv/pipx
|
|
62
|
+
rc = (self.root / ".zshrc").read_text()
|
|
63
|
+
self.assertEqual(rc.count(manage.BEGIN), 1)
|
|
64
|
+
self.assertIn(tool[0], rc)
|
|
65
|
+
hooks = json.loads((self.root / ".codex/hooks.json").read_text())
|
|
66
|
+
commands = [h["hooks"][0]["command"] for h in hooks["hooks"]["SessionStart"]]
|
|
67
|
+
self.assertEqual(commands, [tool[0] + " hook"])
|
|
68
|
+
manage.installation(self.args, True)
|
|
69
|
+
self.assertEqual((self.root / ".zshrc").read_text(), "# mine\n")
|
|
70
|
+
|
|
56
71
|
def test_install_repeat_uninstall_preserves_later_changes(self):
|
|
57
72
|
original = "# original rc\nexport USER_SETTING=yes\n"
|
|
58
73
|
(self.root / ".zshrc").write_text(original)
|
|
@@ -116,6 +116,8 @@ class StartupTests(unittest.TestCase):
|
|
|
116
116
|
)
|
|
117
117
|
conf = (root / "tmux.conf").read_text()
|
|
118
118
|
self.assertIn("CTX USED", conf)
|
|
119
|
+
# Codex asks tmux for #{mouse}; off makes the wheel recall old prompts.
|
|
120
|
+
self.assertIn("set -g mouse on", conf)
|
|
119
121
|
self.assertNotIn("binding pending", conf)
|
|
120
122
|
# error.json is written before the fallback status is applied.
|
|
121
123
|
# Faster Linux startup can observe that intermediate state.
|
|
@@ -27,11 +27,13 @@ class UpdateTests(unittest.TestCase):
|
|
|
27
27
|
with tempfile.TemporaryDirectory() as tmp:
|
|
28
28
|
(Path(tmp) / "uv-receipt.toml").touch()
|
|
29
29
|
self.assertEqual(
|
|
30
|
-
upgrade_command(tmp, pkg), ["uv", "tool", "
|
|
30
|
+
upgrade_command(tmp, pkg), ["uv", "tool", "install", "codex-statusline@latest"]
|
|
31
31
|
)
|
|
32
32
|
with tempfile.TemporaryDirectory() as tmp:
|
|
33
33
|
(Path(tmp) / "pipx_metadata.json").touch()
|
|
34
|
-
self.assertEqual(
|
|
34
|
+
self.assertEqual(
|
|
35
|
+
upgrade_command(tmp, pkg), ["pipx", "install", "--force", "codex-statusline"]
|
|
36
|
+
)
|
|
35
37
|
self.assertEqual(upgrade_command("/nowhere", pkg)[1:3], ["-m", "pip"])
|
|
36
38
|
repo = Path(__file__).resolve().parent.parent
|
|
37
39
|
if (repo / ".git").exists():
|
|
@@ -40,7 +42,7 @@ class UpdateTests(unittest.TestCase):
|
|
|
40
42
|
def test_update_hint_is_dropped_first_when_narrow(self):
|
|
41
43
|
state = {"context_used": 10, "now": 1000, "update": "0.2.0", "quotas": {}}
|
|
42
44
|
wide = render(state, 240, tmux=False)
|
|
43
|
-
self.assertIn("↑
|
|
44
|
-
self.assertIn("^
|
|
45
|
-
self.assertNotIn("
|
|
46
|
-
self.assertNotIn("
|
|
45
|
+
self.assertIn("↑ update available: codex-statusline update", wide)
|
|
46
|
+
self.assertIn("^ update available", render(state, 240, tmux=False, ascii_only=True))
|
|
47
|
+
self.assertNotIn("update", render(state, 40, tmux=False))
|
|
48
|
+
self.assertNotIn("update", render(dict(state, update="#(evil)"), 240, tmux=False))
|
|
File without changes
|
{codex_statusline-0.1.3 → codex_statusline-0.2.2}/codex_statusline.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{codex_statusline-0.1.3 → codex_statusline-0.2.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
|