ring-cli 0.5.0__tar.gz → 0.6.0__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.
- {ring_cli-0.5.0 → ring_cli-0.6.0}/PKG-INFO +86 -5
- {ring_cli-0.5.0 → ring_cli-0.6.0}/README.en.md +85 -4
- {ring_cli-0.5.0 → ring_cli-0.6.0}/pyproject.toml +17 -3
- {ring_cli-0.5.0 → ring_cli-0.6.0}/src/ring/cli.py +98 -3
- ring_cli-0.6.0/src/ring/commands/completion.py +167 -0
- ring_cli-0.6.0/src/ring/commands/stats.py +74 -0
- {ring_cli-0.5.0 → ring_cli-0.6.0}/src/ring/config.py +16 -0
- {ring_cli-0.5.0 → ring_cli-0.6.0}/src/ring/hook.py +19 -0
- {ring_cli-0.5.0 → ring_cli-0.6.0}/src/ring/hook_protocol.py +44 -0
- ring_cli-0.6.0/src/ring/locale/en/LC_MESSAGES/ring.mo +0 -0
- {ring_cli-0.5.0 → ring_cli-0.6.0}/src/ring/locale/en/LC_MESSAGES/ring.po +151 -5
- {ring_cli-0.5.0 → ring_cli-0.6.0}/src/ring/locale/ring.pot +340 -184
- {ring_cli-0.5.0 → ring_cli-0.6.0}/src/ring/notify/__init__.py +31 -4
- {ring_cli-0.5.0 → ring_cli-0.6.0}/src/ring/notify/base.py +16 -3
- {ring_cli-0.5.0 → ring_cli-0.6.0}/src/ring/notify/notify_send.py +2 -3
- ring_cli-0.6.0/src/ring/notify/ntfy.py +66 -0
- {ring_cli-0.5.0 → ring_cli-0.6.0}/src/ring/notify/osascript_notifier.py +2 -3
- {ring_cli-0.5.0 → ring_cli-0.6.0}/src/ring/notify/terminal_notifier.py +2 -3
- ring_cli-0.6.0/src/ring/notify/webhook.py +63 -0
- ring_cli-0.6.0/src/ring/plugins.py +75 -0
- {ring_cli-0.5.0 → ring_cli-0.6.0}/src/ring/registry.py +2 -0
- ring_cli-0.6.0/src/ring/stats.py +152 -0
- {ring_cli-0.5.0 → ring_cli-0.6.0}/src/ring/tui.py +31 -4
- ring_cli-0.5.0/src/ring/locale/en/LC_MESSAGES/ring.mo +0 -0
- {ring_cli-0.5.0 → ring_cli-0.6.0}/LICENSE +0 -0
- {ring_cli-0.5.0 → ring_cli-0.6.0}/src/ring/__init__.py +0 -0
- {ring_cli-0.5.0 → ring_cli-0.6.0}/src/ring/__main__.py +0 -0
- {ring_cli-0.5.0 → ring_cli-0.6.0}/src/ring/commands/__init__.py +0 -0
- {ring_cli-0.5.0 → ring_cli-0.6.0}/src/ring/commands/_args.py +0 -0
- {ring_cli-0.5.0 → ring_cli-0.6.0}/src/ring/commands/doctor.py +0 -0
- {ring_cli-0.5.0 → ring_cli-0.6.0}/src/ring/commands/focus.py +0 -0
- {ring_cli-0.5.0 → ring_cli-0.6.0}/src/ring/commands/gc.py +0 -0
- {ring_cli-0.5.0 → ring_cli-0.6.0}/src/ring/commands/hook.py +0 -0
- {ring_cli-0.5.0 → ring_cli-0.6.0}/src/ring/focus/__init__.py +0 -0
- {ring_cli-0.5.0 → ring_cli-0.6.0}/src/ring/focus/applescript.py +0 -0
- {ring_cli-0.5.0 → ring_cli-0.6.0}/src/ring/focus/base.py +0 -0
- {ring_cli-0.5.0 → ring_cli-0.6.0}/src/ring/focus/iterm2.py +0 -0
- {ring_cli-0.5.0 → ring_cli-0.6.0}/src/ring/focus/linux_wm.py +0 -0
- {ring_cli-0.5.0 → ring_cli-0.6.0}/src/ring/focus/terminal.py +0 -0
- {ring_cli-0.5.0 → ring_cli-0.6.0}/src/ring/focus/tmux.py +0 -0
- {ring_cli-0.5.0 → ring_cli-0.6.0}/src/ring/gc.py +0 -0
- {ring_cli-0.5.0 → ring_cli-0.6.0}/src/ring/i18n.py +0 -0
- {ring_cli-0.5.0 → ring_cli-0.6.0}/src/ring/ipc.py +0 -0
- {ring_cli-0.5.0 → ring_cli-0.6.0}/src/ring/labels.py +0 -0
- {ring_cli-0.5.0 → ring_cli-0.6.0}/src/ring/notify/command.py +0 -0
- {ring_cli-0.5.0 → ring_cli-0.6.0}/src/ring/osascript.py +0 -0
- {ring_cli-0.5.0 → ring_cli-0.6.0}/src/ring/sources/__init__.py +0 -0
- {ring_cli-0.5.0 → ring_cli-0.6.0}/src/ring/sources/base.py +0 -0
- {ring_cli-0.5.0 → ring_cli-0.6.0}/src/ring/sources/claude_code.py +0 -0
- {ring_cli-0.5.0 → ring_cli-0.6.0}/src/ring/sources/codex.py +0 -0
- {ring_cli-0.5.0 → ring_cli-0.6.0}/src/ring/sources/hook_registry.py +0 -0
- {ring_cli-0.5.0 → ring_cli-0.6.0}/src/ring/transcript.py +0 -0
- {ring_cli-0.5.0 → ring_cli-0.6.0}/src/ring/watcher.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ring-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: RiNG — Realtime Instance Notification Grid for active agent CLI sessions.
|
|
5
5
|
Keywords: claude-code,codex,tui,dashboard,session,monitor,rich
|
|
6
6
|
Author: Wei Lee
|
|
@@ -59,7 +59,10 @@ RiNG puts them on one board, with sessions waiting for you sorted first.
|
|
|
59
59
|
- **Waiting first**: sessions that need your response are highlighted and sorted above the rest.
|
|
60
60
|
- **Jump back to the terminal**: in the TUI, select a session and press `Enter` / `Space` to focus tmux, iTerm2, Terminal.app (macOS), or a Linux X11 window (`wmctrl`).
|
|
61
61
|
- **Notifies you without the board open**: with hooks installed, the moment a session turns 🔴 waiting it beeps and fires a system notification — even with no RiNG board running. With `terminal-notifier`, clicking the notification jumps back.
|
|
62
|
-
- **Name your sessions**: press `n` in the TUI to
|
|
62
|
+
- **Name your sessions**: press `n` in the TUI to name a session, e.g. "auth refactor"; once named, the board and notifications show the name instead of the workspace directory.
|
|
63
|
+
- **See what it is waiting for**: in hook mode, a 🔴 waiting session carries the concrete pending item (the command to run, the question asked) — shown in the TUI and in the notification body.
|
|
64
|
+
- **Fits your status bar**: `ring --format oneline` prints a `🔴2 🟢1 🟡3` one-liner for tmux / SwiftBar / waybar; `--format json` feeds scripts.
|
|
65
|
+
- **Rings your phone too**: built-in ntfy / webhook notifiers push 🔴 waiting to your phone when you are away from the desk.
|
|
63
66
|
- **Local and extensible**: RiNG only reads local Claude Code / Codex data and writes `~/.config/ring/`; session sources, focusers, and notifiers are pluggable.
|
|
64
67
|
|
|
65
68
|
## Run
|
|
@@ -119,12 +122,39 @@ Hooks only apply to new sessions, so restart Claude Code / Codex sessions after
|
|
|
119
122
|
| `ring doctor` | Read-only environment diagnosis |
|
|
120
123
|
| `ring gc --dry-run` | Preview RiNG-owned stale state cleanup |
|
|
121
124
|
| `ring gc` | Clean RiNG-owned stale state files |
|
|
125
|
+
| `ring --format json` | Machine-readable board snapshot (for jq / scripts) |
|
|
126
|
+
| `ring --format oneline` | `🔴2 🟢1 🟡3` one-liner (for status bars) |
|
|
127
|
+
| `ring stats` | Waiting stats: how long agents kept 🔴 waiting in the last 7 days |
|
|
128
|
+
| `ring completion zsh` | Print a shell completion script (zsh / bash) |
|
|
129
|
+
|
|
130
|
+
### Status Bar Integration (`--format`)
|
|
131
|
+
|
|
132
|
+
```sh
|
|
133
|
+
ring --format oneline # 🔴2 🟢1 🟡3 (empty output when no sessions, so the segment collapses)
|
|
134
|
+
ring --format json | jq '.counts.waiting'
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
- **tmux**: `set -g status-right '#(ring --format oneline) …'` (with `status-interval 5`).
|
|
138
|
+
- **SwiftBar / xbar / waybar**: wrap `ring --format oneline` or consume the JSON.
|
|
139
|
+
- JSON keys are a stable interface (additive only), safe to script against.
|
|
140
|
+
|
|
141
|
+
### Shell Completion (`ring completion`)
|
|
142
|
+
|
|
143
|
+
```sh
|
|
144
|
+
# ~/.zshrc
|
|
145
|
+
eval "$(ring completion zsh)"
|
|
146
|
+
# ~/.bashrc
|
|
147
|
+
eval "$(ring completion bash)"
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Completes subcommands, flags, and `config set` keys.
|
|
122
151
|
|
|
123
152
|
## Watch Mode
|
|
124
153
|
|
|
125
154
|
- With **Textual** (`[tui]` extra) in a real terminal: interactive TUI.
|
|
126
155
|
Use `↑/↓` to select, `Enter` / `Space` to jump, `n` to name a session, `a` to toggle ended sessions, `r` to refresh, and `q` to quit.
|
|
127
156
|
If you have vim muscle memory like I do, `j/k` move up/down and `g/G` jump to the first/last row.
|
|
157
|
+
When the selected row is 🔴 waiting, a line under the table shows **what it is concretely waiting for** (the command to run, the question asked; hook mode only).
|
|
128
158
|
- Otherwise: Rich polling; without Rich, plain text.
|
|
129
159
|
|
|
130
160
|
### Jump To A Session
|
|
@@ -155,6 +185,20 @@ Without it, RiNG falls back to macOS text notifications without click-to-focus.
|
|
|
155
185
|
Notification sound, repeat timing, and backend selection are configurable; notification backends
|
|
156
186
|
are also pluggable via the `Notifier` extension point.
|
|
157
187
|
|
|
188
|
+
#### Push To Your Phone (ntfy / webhook)
|
|
189
|
+
|
|
190
|
+
Desktop notifications do not help when you are away from the desk. Point RiNG at an
|
|
191
|
+
[ntfy](https://ntfy.sh) topic to push to your phone:
|
|
192
|
+
|
|
193
|
+
```toml
|
|
194
|
+
# ~/.config/ring/config.toml
|
|
195
|
+
notify_ntfy_url = "https://ntfy.sh/my-ring-topic" # subscribe to the same topic in the ntfy app
|
|
196
|
+
notify_also = ["ntfy"] # desktop notification as usual, plus a copy to the phone
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
`notify_backend = "ntfy"` pushes to the phone only. For Slack / your own bot / IFTTT, set
|
|
200
|
+
`notify_webhook_url` to use the generic webhook backend (JSON POST with a stable, additive-only payload).
|
|
201
|
+
|
|
158
202
|
### Cleaning RiNG State Files (`ring gc`)
|
|
159
203
|
|
|
160
204
|
When RiNG receives `SessionEnd`, it removes its own hook registry entry. If an agent crashes or the
|
|
@@ -171,6 +215,19 @@ ring gc --all-ended # delete every registry file currently classified as en
|
|
|
171
215
|
`ring gc` only cleans state files RiNG owns under `~/.config/ring/`. It does not touch Claude Code /
|
|
172
216
|
Codex transcripts or state. `ring doctor` remains read-only and never deletes files.
|
|
173
217
|
|
|
218
|
+
### Waiting Stats (`ring stats`)
|
|
219
|
+
|
|
220
|
+
In hook mode, RiNG logs session **state transitions** to `~/.config/ring/events.jsonl`
|
|
221
|
+
(transitions only, tiny, self-trimming past a size cap). `ring stats` then tells you, per project,
|
|
222
|
+
how many times an agent 🔴 waited on you and for how long (avg / max / total).
|
|
223
|
+
|
|
224
|
+
```sh
|
|
225
|
+
ring stats # last 7 days
|
|
226
|
+
ring stats --since 12h # custom window
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Like precise notifications, 🔴 waiting is invisible to zero-config, so stats also needs hook mode.
|
|
230
|
+
|
|
174
231
|
## Session Sources
|
|
175
232
|
|
|
176
233
|
RiNG collects sessions from registered sources. Built-ins:
|
|
@@ -311,7 +368,11 @@ notify_ignore_dnd = false
|
|
|
311
368
|
notify_backend = "auto" # auto / terminal-notifier / osascript / notify-send / agent-hooks / none
|
|
312
369
|
notify_repeat_seconds = [30, 120, 300]
|
|
313
370
|
notify_repeat_max = 3
|
|
371
|
+
notify_ntfy_url = "" # full ntfy topic URL enables phone push (e.g. https://ntfy.sh/my-topic)
|
|
372
|
+
notify_webhook_url = "" # URL enables the generic webhook backend (JSON POST)
|
|
373
|
+
notify_also = [] # extra backends fired besides the primary, e.g. ["ntfy"]
|
|
314
374
|
focusers = ["tmux", "iTerm2", "Terminal", "linux-wm"]
|
|
375
|
+
plugins = [] # external plugin modules imported at startup (see Extending)
|
|
315
376
|
|
|
316
377
|
[colors]
|
|
317
378
|
waiting = "bold red"
|
|
@@ -331,15 +392,35 @@ RiNG is not tied to a specific tool or terminal.
|
|
|
331
392
|
|-----------------|---------|-----------|
|
|
332
393
|
| `SessionSource` | find sessions | Claude Code, Codex, hook registry |
|
|
333
394
|
| `Focuser` | jump to terminals | tmux, iTerm2, Terminal.app, Linux X11 (wmctrl) |
|
|
334
|
-
| `Notifier` | notify when sessions are waiting | terminal-notifier, osascript, notify-send |
|
|
395
|
+
| `Notifier` | notify when sessions are waiting | terminal-notifier, osascript, notify-send, ntfy, webhook |
|
|
335
396
|
|
|
336
397
|
Each backend is a small module under `ring/sources/`, `ring/focus/`, or `ring/notify/`, registered via `register_*()`.
|
|
337
398
|
|
|
399
|
+
### Loading Your Plugin Into An Installed `ring`
|
|
400
|
+
|
|
401
|
+
`register_*()` only counts if something runs it. The installed `ring` command loads plugins from
|
|
402
|
+
two places at startup:
|
|
403
|
+
|
|
404
|
+
1. **Entry point** (for published packages) — declare it in your package's `pyproject.toml`,
|
|
405
|
+
pointing at a module or callable (a module registers on import; a callable is invoked once
|
|
406
|
+
with no arguments):
|
|
407
|
+
|
|
408
|
+
```toml
|
|
409
|
+
[project.entry-points."ring.plugins"]
|
|
410
|
+
mytool = "ring_mytool.plugin"
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
2. **Config** (for local scripts) — add `plugins = ["my_module"]` to `~/.config/ring/config.toml`;
|
|
414
|
+
the module must be importable (site-packages or `PYTHONPATH`).
|
|
415
|
+
|
|
416
|
+
A broken plugin prints one warning line to stderr and never blocks the board.
|
|
417
|
+
|
|
338
418
|
## Platform & Privacy
|
|
339
419
|
|
|
340
420
|
- **Platform**: macOS / Linux. Windows is not supported.
|
|
341
|
-
- **Privacy**:
|
|
342
|
-
`~/.config/ring/`. No network
|
|
421
|
+
- **Privacy**: local by default. RiNG only reads local `~/.claude/` and `~/.codex/` data and writes
|
|
422
|
+
`~/.config/ring/`. No telemetry. The only network calls are the optional ntfy / webhook notifiers,
|
|
423
|
+
and only to URLs you configure yourself.
|
|
343
424
|
|
|
344
425
|
## Name
|
|
345
426
|
|
|
@@ -34,7 +34,10 @@ RiNG puts them on one board, with sessions waiting for you sorted first.
|
|
|
34
34
|
- **Waiting first**: sessions that need your response are highlighted and sorted above the rest.
|
|
35
35
|
- **Jump back to the terminal**: in the TUI, select a session and press `Enter` / `Space` to focus tmux, iTerm2, Terminal.app (macOS), or a Linux X11 window (`wmctrl`).
|
|
36
36
|
- **Notifies you without the board open**: with hooks installed, the moment a session turns 🔴 waiting it beeps and fires a system notification — even with no RiNG board running. With `terminal-notifier`, clicking the notification jumps back.
|
|
37
|
-
- **Name your sessions**: press `n` in the TUI to
|
|
37
|
+
- **Name your sessions**: press `n` in the TUI to name a session, e.g. "auth refactor"; once named, the board and notifications show the name instead of the workspace directory.
|
|
38
|
+
- **See what it is waiting for**: in hook mode, a 🔴 waiting session carries the concrete pending item (the command to run, the question asked) — shown in the TUI and in the notification body.
|
|
39
|
+
- **Fits your status bar**: `ring --format oneline` prints a `🔴2 🟢1 🟡3` one-liner for tmux / SwiftBar / waybar; `--format json` feeds scripts.
|
|
40
|
+
- **Rings your phone too**: built-in ntfy / webhook notifiers push 🔴 waiting to your phone when you are away from the desk.
|
|
38
41
|
- **Local and extensible**: RiNG only reads local Claude Code / Codex data and writes `~/.config/ring/`; session sources, focusers, and notifiers are pluggable.
|
|
39
42
|
|
|
40
43
|
## Run
|
|
@@ -94,12 +97,39 @@ Hooks only apply to new sessions, so restart Claude Code / Codex sessions after
|
|
|
94
97
|
| `ring doctor` | Read-only environment diagnosis |
|
|
95
98
|
| `ring gc --dry-run` | Preview RiNG-owned stale state cleanup |
|
|
96
99
|
| `ring gc` | Clean RiNG-owned stale state files |
|
|
100
|
+
| `ring --format json` | Machine-readable board snapshot (for jq / scripts) |
|
|
101
|
+
| `ring --format oneline` | `🔴2 🟢1 🟡3` one-liner (for status bars) |
|
|
102
|
+
| `ring stats` | Waiting stats: how long agents kept 🔴 waiting in the last 7 days |
|
|
103
|
+
| `ring completion zsh` | Print a shell completion script (zsh / bash) |
|
|
104
|
+
|
|
105
|
+
### Status Bar Integration (`--format`)
|
|
106
|
+
|
|
107
|
+
```sh
|
|
108
|
+
ring --format oneline # 🔴2 🟢1 🟡3 (empty output when no sessions, so the segment collapses)
|
|
109
|
+
ring --format json | jq '.counts.waiting'
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
- **tmux**: `set -g status-right '#(ring --format oneline) …'` (with `status-interval 5`).
|
|
113
|
+
- **SwiftBar / xbar / waybar**: wrap `ring --format oneline` or consume the JSON.
|
|
114
|
+
- JSON keys are a stable interface (additive only), safe to script against.
|
|
115
|
+
|
|
116
|
+
### Shell Completion (`ring completion`)
|
|
117
|
+
|
|
118
|
+
```sh
|
|
119
|
+
# ~/.zshrc
|
|
120
|
+
eval "$(ring completion zsh)"
|
|
121
|
+
# ~/.bashrc
|
|
122
|
+
eval "$(ring completion bash)"
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Completes subcommands, flags, and `config set` keys.
|
|
97
126
|
|
|
98
127
|
## Watch Mode
|
|
99
128
|
|
|
100
129
|
- With **Textual** (`[tui]` extra) in a real terminal: interactive TUI.
|
|
101
130
|
Use `↑/↓` to select, `Enter` / `Space` to jump, `n` to name a session, `a` to toggle ended sessions, `r` to refresh, and `q` to quit.
|
|
102
131
|
If you have vim muscle memory like I do, `j/k` move up/down and `g/G` jump to the first/last row.
|
|
132
|
+
When the selected row is 🔴 waiting, a line under the table shows **what it is concretely waiting for** (the command to run, the question asked; hook mode only).
|
|
103
133
|
- Otherwise: Rich polling; without Rich, plain text.
|
|
104
134
|
|
|
105
135
|
### Jump To A Session
|
|
@@ -130,6 +160,20 @@ Without it, RiNG falls back to macOS text notifications without click-to-focus.
|
|
|
130
160
|
Notification sound, repeat timing, and backend selection are configurable; notification backends
|
|
131
161
|
are also pluggable via the `Notifier` extension point.
|
|
132
162
|
|
|
163
|
+
#### Push To Your Phone (ntfy / webhook)
|
|
164
|
+
|
|
165
|
+
Desktop notifications do not help when you are away from the desk. Point RiNG at an
|
|
166
|
+
[ntfy](https://ntfy.sh) topic to push to your phone:
|
|
167
|
+
|
|
168
|
+
```toml
|
|
169
|
+
# ~/.config/ring/config.toml
|
|
170
|
+
notify_ntfy_url = "https://ntfy.sh/my-ring-topic" # subscribe to the same topic in the ntfy app
|
|
171
|
+
notify_also = ["ntfy"] # desktop notification as usual, plus a copy to the phone
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`notify_backend = "ntfy"` pushes to the phone only. For Slack / your own bot / IFTTT, set
|
|
175
|
+
`notify_webhook_url` to use the generic webhook backend (JSON POST with a stable, additive-only payload).
|
|
176
|
+
|
|
133
177
|
### Cleaning RiNG State Files (`ring gc`)
|
|
134
178
|
|
|
135
179
|
When RiNG receives `SessionEnd`, it removes its own hook registry entry. If an agent crashes or the
|
|
@@ -146,6 +190,19 @@ ring gc --all-ended # delete every registry file currently classified as en
|
|
|
146
190
|
`ring gc` only cleans state files RiNG owns under `~/.config/ring/`. It does not touch Claude Code /
|
|
147
191
|
Codex transcripts or state. `ring doctor` remains read-only and never deletes files.
|
|
148
192
|
|
|
193
|
+
### Waiting Stats (`ring stats`)
|
|
194
|
+
|
|
195
|
+
In hook mode, RiNG logs session **state transitions** to `~/.config/ring/events.jsonl`
|
|
196
|
+
(transitions only, tiny, self-trimming past a size cap). `ring stats` then tells you, per project,
|
|
197
|
+
how many times an agent 🔴 waited on you and for how long (avg / max / total).
|
|
198
|
+
|
|
199
|
+
```sh
|
|
200
|
+
ring stats # last 7 days
|
|
201
|
+
ring stats --since 12h # custom window
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Like precise notifications, 🔴 waiting is invisible to zero-config, so stats also needs hook mode.
|
|
205
|
+
|
|
149
206
|
## Session Sources
|
|
150
207
|
|
|
151
208
|
RiNG collects sessions from registered sources. Built-ins:
|
|
@@ -286,7 +343,11 @@ notify_ignore_dnd = false
|
|
|
286
343
|
notify_backend = "auto" # auto / terminal-notifier / osascript / notify-send / agent-hooks / none
|
|
287
344
|
notify_repeat_seconds = [30, 120, 300]
|
|
288
345
|
notify_repeat_max = 3
|
|
346
|
+
notify_ntfy_url = "" # full ntfy topic URL enables phone push (e.g. https://ntfy.sh/my-topic)
|
|
347
|
+
notify_webhook_url = "" # URL enables the generic webhook backend (JSON POST)
|
|
348
|
+
notify_also = [] # extra backends fired besides the primary, e.g. ["ntfy"]
|
|
289
349
|
focusers = ["tmux", "iTerm2", "Terminal", "linux-wm"]
|
|
350
|
+
plugins = [] # external plugin modules imported at startup (see Extending)
|
|
290
351
|
|
|
291
352
|
[colors]
|
|
292
353
|
waiting = "bold red"
|
|
@@ -306,15 +367,35 @@ RiNG is not tied to a specific tool or terminal.
|
|
|
306
367
|
|-----------------|---------|-----------|
|
|
307
368
|
| `SessionSource` | find sessions | Claude Code, Codex, hook registry |
|
|
308
369
|
| `Focuser` | jump to terminals | tmux, iTerm2, Terminal.app, Linux X11 (wmctrl) |
|
|
309
|
-
| `Notifier` | notify when sessions are waiting | terminal-notifier, osascript, notify-send |
|
|
370
|
+
| `Notifier` | notify when sessions are waiting | terminal-notifier, osascript, notify-send, ntfy, webhook |
|
|
310
371
|
|
|
311
372
|
Each backend is a small module under `ring/sources/`, `ring/focus/`, or `ring/notify/`, registered via `register_*()`.
|
|
312
373
|
|
|
374
|
+
### Loading Your Plugin Into An Installed `ring`
|
|
375
|
+
|
|
376
|
+
`register_*()` only counts if something runs it. The installed `ring` command loads plugins from
|
|
377
|
+
two places at startup:
|
|
378
|
+
|
|
379
|
+
1. **Entry point** (for published packages) — declare it in your package's `pyproject.toml`,
|
|
380
|
+
pointing at a module or callable (a module registers on import; a callable is invoked once
|
|
381
|
+
with no arguments):
|
|
382
|
+
|
|
383
|
+
```toml
|
|
384
|
+
[project.entry-points."ring.plugins"]
|
|
385
|
+
mytool = "ring_mytool.plugin"
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
2. **Config** (for local scripts) — add `plugins = ["my_module"]` to `~/.config/ring/config.toml`;
|
|
389
|
+
the module must be importable (site-packages or `PYTHONPATH`).
|
|
390
|
+
|
|
391
|
+
A broken plugin prints one warning line to stderr and never blocks the board.
|
|
392
|
+
|
|
313
393
|
## Platform & Privacy
|
|
314
394
|
|
|
315
395
|
- **Platform**: macOS / Linux. Windows is not supported.
|
|
316
|
-
- **Privacy**:
|
|
317
|
-
`~/.config/ring/`. No network
|
|
396
|
+
- **Privacy**: local by default. RiNG only reads local `~/.claude/` and `~/.codex/` data and writes
|
|
397
|
+
`~/.config/ring/`. No telemetry. The only network calls are the optional ntfy / webhook notifiers,
|
|
398
|
+
and only to URLs you configure yourself.
|
|
318
399
|
|
|
319
400
|
## Name
|
|
320
401
|
|
|
@@ -2,14 +2,22 @@
|
|
|
2
2
|
# import 的 module(見 [tool.uv.build-backend] module-name)與 CLI 指令仍是 `ring`。
|
|
3
3
|
[project]
|
|
4
4
|
name = "ring-cli"
|
|
5
|
-
version = "0.
|
|
5
|
+
version = "0.6.0"
|
|
6
6
|
description = "RiNG — Realtime Instance Notification Grid for active agent CLI sessions."
|
|
7
7
|
readme = "README.en.md"
|
|
8
8
|
requires-python = ">=3.13"
|
|
9
9
|
license = "MIT"
|
|
10
10
|
license-files = ["LICENSE"]
|
|
11
11
|
authors = [{ name = "Wei Lee", email = "weilee.rx@gmail.com" }]
|
|
12
|
-
keywords = [
|
|
12
|
+
keywords = [
|
|
13
|
+
"claude-code",
|
|
14
|
+
"codex",
|
|
15
|
+
"tui",
|
|
16
|
+
"dashboard",
|
|
17
|
+
"session",
|
|
18
|
+
"monitor",
|
|
19
|
+
"rich",
|
|
20
|
+
]
|
|
13
21
|
classifiers = [
|
|
14
22
|
"Development Status :: 4 - Beta",
|
|
15
23
|
"Environment :: Console",
|
|
@@ -40,7 +48,13 @@ dev = [
|
|
|
40
48
|
{ include-group = "linters" },
|
|
41
49
|
]
|
|
42
50
|
base = ["poethepoet>=0.41.0", "babel>=2.14"]
|
|
43
|
-
test = [
|
|
51
|
+
test = [
|
|
52
|
+
"pytest>=9",
|
|
53
|
+
"pytest-cov>=4",
|
|
54
|
+
"pytest-xdist>=3.1.0",
|
|
55
|
+
"pytest-asyncio>=0.24",
|
|
56
|
+
"textual>=0.80",
|
|
57
|
+
]
|
|
44
58
|
linters = ["ruff>=0.15.1", "mypy>=1.19.1", "prek>=0.3.3", "commitizen>=4.13.9"]
|
|
45
59
|
|
|
46
60
|
[build-system]
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
12
|
import argparse
|
|
13
|
+
import json
|
|
13
14
|
import sys
|
|
14
15
|
import time
|
|
15
16
|
from dataclasses import fields
|
|
@@ -18,10 +19,12 @@ from typing import Any
|
|
|
18
19
|
|
|
19
20
|
from ring import __version__
|
|
20
21
|
from ring.commands._args import strip_lang as _strip_lang
|
|
22
|
+
from ring.commands.completion import run_completion
|
|
21
23
|
from ring.commands.doctor import run_doctor
|
|
22
24
|
from ring.commands.focus import run_focus
|
|
23
25
|
from ring.commands.gc import run_gc
|
|
24
26
|
from ring.commands.hook import run_hook_command, run_install_hooks, run_remove_hooks
|
|
27
|
+
from ring.commands.stats import run_stats
|
|
25
28
|
from ring.config import CONFIG_PATH as CONFIG_PATH
|
|
26
29
|
from ring.config import Config as Config
|
|
27
30
|
from ring.config import ConfigError as ConfigError
|
|
@@ -30,6 +33,7 @@ from ring.config import set_value as set_value
|
|
|
30
33
|
from ring.i18n import gettext as _
|
|
31
34
|
from ring.i18n import ngettext, set_lang
|
|
32
35
|
from ring.labels import load_labels
|
|
36
|
+
from ring.plugins import load_plugins
|
|
33
37
|
from ring.registry import Session, Status, running_agent_pids
|
|
34
38
|
from ring.sources import discover_sessions
|
|
35
39
|
|
|
@@ -134,8 +138,12 @@ def provider_label(provider: str) -> str:
|
|
|
134
138
|
|
|
135
139
|
|
|
136
140
|
def labeled_project(project: str, label: str) -> str:
|
|
137
|
-
"""
|
|
138
|
-
|
|
141
|
+
"""看板顯示名:使用者取過名(TUI 按 ``n``)就用名字,否則用專案(目錄)名。
|
|
142
|
+
|
|
143
|
+
名字本來就是「這個 session 在做什麼」的更精準描述,取了就直接取代 workspace 名,
|
|
144
|
+
不再併排(``maigo · 重構登入`` 太佔欄寬)。
|
|
145
|
+
"""
|
|
146
|
+
return label or project
|
|
139
147
|
|
|
140
148
|
|
|
141
149
|
def show_tool_column(sessions: list[Session]) -> bool:
|
|
@@ -234,6 +242,55 @@ def _render_plain(sessions: list[Session], show_legend: bool, show_tool: bool =
|
|
|
234
242
|
return "\n".join(lines)
|
|
235
243
|
|
|
236
244
|
|
|
245
|
+
# ----------------------------------------------------------------------------- machine-readable
|
|
246
|
+
def render_json(sessions: list[Session]) -> str:
|
|
247
|
+
"""整個看板的機器可讀快照(給 jq / 腳本 / 自訂 status bar widget 用)。
|
|
248
|
+
|
|
249
|
+
鍵名視為穩定介面:只加不改。``label`` 是使用者在 TUI 按 ``n`` 取的名字,沒取過為空字串。
|
|
250
|
+
"""
|
|
251
|
+
labels = load_labels()
|
|
252
|
+
data = {
|
|
253
|
+
"generated_at": time.time(),
|
|
254
|
+
"agent_processes": len(running_agent_pids()),
|
|
255
|
+
"counts": {st.value: sum(1 for s in sessions if s.status is st) for st in Status},
|
|
256
|
+
"sessions": [
|
|
257
|
+
{
|
|
258
|
+
"session_id": s.session_id,
|
|
259
|
+
"provider": s.provider,
|
|
260
|
+
"project": s.project,
|
|
261
|
+
"label": labels.get(s.session_id, ""),
|
|
262
|
+
"status": s.status.value,
|
|
263
|
+
"marker": s.status.marker,
|
|
264
|
+
"cwd": s.cwd,
|
|
265
|
+
"location": s.location,
|
|
266
|
+
"tmux_target": s.tmux_target,
|
|
267
|
+
"tty": s.tty,
|
|
268
|
+
"idle_seconds": round(s.idle_for, 1),
|
|
269
|
+
"last_active": s.last_active,
|
|
270
|
+
"last_action": s.last_action,
|
|
271
|
+
"waiting_detail": s.waiting_detail,
|
|
272
|
+
"todo": {"done": s.todo[0], "total": s.todo[1]} if s.todo else None,
|
|
273
|
+
"source": s.source,
|
|
274
|
+
}
|
|
275
|
+
for s in sessions
|
|
276
|
+
],
|
|
277
|
+
}
|
|
278
|
+
return json.dumps(data, ensure_ascii=False, indent=2)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def render_oneline(sessions: list[Session]) -> str:
|
|
282
|
+
"""超精簡單行摘要(給 tmux status bar / SwiftBar / waybar 用):``🔴2 🟢1 🟡3``。
|
|
283
|
+
|
|
284
|
+
只列非零狀態;完全沒 session 時回空字串,讓 status bar 段落自然收起來。
|
|
285
|
+
"""
|
|
286
|
+
parts = [
|
|
287
|
+
f"{st.marker}{n}"
|
|
288
|
+
for st in (Status.WAITING, Status.WORKING, Status.IDLE, Status.ENDED)
|
|
289
|
+
if (n := sum(1 for s in sessions if s.status is st))
|
|
290
|
+
]
|
|
291
|
+
return " ".join(parts)
|
|
292
|
+
|
|
293
|
+
|
|
237
294
|
# ----------------------------------------------------------------------------- entry
|
|
238
295
|
def print_snapshot(sessions: list[Session], show_legend: bool) -> None:
|
|
239
296
|
show_tool = show_tool_column(sessions)
|
|
@@ -389,6 +446,8 @@ commands:
|
|
|
389
446
|
focus SESSION_ID 聚焦指定 session;TUI 在跑時會回到 RiNG 並選中該列
|
|
390
447
|
gc [--dry-run] 清理 RiNG 自己的 stale 狀態檔
|
|
391
448
|
doctor 顯示環境診斷(唯讀)——hook、通知、focuser、維護提示
|
|
449
|
+
stats [--since 7d] 等待統計:你讓 agent 🔴 等了多久(hook 模式)
|
|
450
|
+
completion SHELL 印出 shell 補全腳本(zsh / bash)
|
|
392
451
|
"""
|
|
393
452
|
)
|
|
394
453
|
|
|
@@ -443,6 +502,22 @@ options:
|
|
|
443
502
|
|
|
444
503
|
唯讀環境診斷:逐節報告 hook 安裝狀態、通知後端可用性、focuser 可用性、維護提示與設定檔位置。
|
|
445
504
|
不寫任何檔案、不安裝、不發通知;固定回傳 0。
|
|
505
|
+
"""
|
|
506
|
+
),
|
|
507
|
+
"stats": _(
|
|
508
|
+
"""usage: ring stats [--since DURATION]
|
|
509
|
+
|
|
510
|
+
等待統計:最近一段時間各專案讓你回應的次數與 🔴 等待時長(平均 / 最長 / 總計)。
|
|
511
|
+
資料來自 hook 寫的狀態轉換 log(~/.config/ring/events.jsonl),zero-config 測不到等待。
|
|
512
|
+
|
|
513
|
+
options:
|
|
514
|
+
--since DURATION 統計時間窗(例如 12h、7d、30d;預設 7d)
|
|
515
|
+
"""
|
|
516
|
+
),
|
|
517
|
+
"completion": _(
|
|
518
|
+
"""usage: ring completion zsh|bash
|
|
519
|
+
|
|
520
|
+
印出 shell 補全腳本。zsh 放 ~/.zshrc:eval "$(ring completion zsh)";bash 同理。
|
|
446
521
|
"""
|
|
447
522
|
),
|
|
448
523
|
}
|
|
@@ -453,10 +528,12 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
453
528
|
raw = list(sys.argv[1:] if argv is None else argv)
|
|
454
529
|
cfg = get_config()
|
|
455
530
|
set_lang(_peek_lang(raw) or cfg.lang) # 在 import ring.tui 前設好,Footer 按鍵說明也跟著語言
|
|
531
|
+
load_plugins() # 在任何 dispatch 之前:hook(notifier plugin)與看板(source plugin)都吃得到
|
|
456
532
|
|
|
457
533
|
if (
|
|
458
534
|
raw
|
|
459
|
-
and raw[0]
|
|
535
|
+
and raw[0]
|
|
536
|
+
in {"hook", "install-hooks", "remove-hooks", "config", "focus", "gc", "doctor", "stats", "completion"}
|
|
460
537
|
and any(arg in {"-h", "--help"} for arg in raw[1:])
|
|
461
538
|
):
|
|
462
539
|
print(_subcommand_help(raw[0]), end="")
|
|
@@ -474,6 +551,10 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
474
551
|
return run_gc(raw[1:])
|
|
475
552
|
if raw and raw[0] == "doctor":
|
|
476
553
|
return run_doctor(raw[1:])
|
|
554
|
+
if raw and raw[0] == "stats":
|
|
555
|
+
return run_stats(raw[1:])
|
|
556
|
+
if raw and raw[0] == "completion":
|
|
557
|
+
return run_completion(raw[1:])
|
|
477
558
|
if raw and raw[0] == "focus" and len(raw) >= 2:
|
|
478
559
|
return run_focus(raw[1:])
|
|
479
560
|
|
|
@@ -495,8 +576,22 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
495
576
|
help=_("顯示顏色圖例(--no-legend 關閉)"),
|
|
496
577
|
)
|
|
497
578
|
parser.add_argument("--lang", help=_("語言(如 en / zh-Hant;也吃 config / RING_LANG / LANG)"))
|
|
579
|
+
parser.add_argument(
|
|
580
|
+
"--format",
|
|
581
|
+
choices=["table", "json", "oneline"],
|
|
582
|
+
default="table",
|
|
583
|
+
help=_("輸出格式:table(預設)/ json(機器可讀)/ oneline(status bar 單行摘要)"),
|
|
584
|
+
)
|
|
498
585
|
args = parser.parse_args(raw)
|
|
499
586
|
|
|
587
|
+
if args.format != "table":
|
|
588
|
+
if args.watch:
|
|
589
|
+
print(_("--format {fmt} 只能用在快照模式,不能配 --watch。", fmt=args.format), file=sys.stderr)
|
|
590
|
+
return 2
|
|
591
|
+
sessions = board(args.all)
|
|
592
|
+
print(render_json(sessions) if args.format == "json" else render_oneline(sessions))
|
|
593
|
+
return 0
|
|
594
|
+
|
|
500
595
|
if args.watch:
|
|
501
596
|
if HAVE_TEXTUAL and sys.stdout.isatty():
|
|
502
597
|
from ring.tui import run_tui
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""``ring completion SHELL``——印出 zsh / bash 補全腳本。
|
|
2
|
+
|
|
3
|
+
CLI 是手寫 dispatch(不是純 argparse subparsers),所以補全腳本也自己生:
|
|
4
|
+
子命令 / 旗標寫死在模板,config 鍵在生成時從 ``settable_keys()`` 動態帶入,
|
|
5
|
+
新增設定鍵不必回來改這裡。用法::
|
|
6
|
+
|
|
7
|
+
# zsh(放 ~/.zshrc)
|
|
8
|
+
eval "$(ring completion zsh)"
|
|
9
|
+
|
|
10
|
+
# bash(放 ~/.bashrc)
|
|
11
|
+
eval "$(ring completion bash)"
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import sys
|
|
17
|
+
|
|
18
|
+
from ring.commands._args import strip_lang as _strip_lang
|
|
19
|
+
from ring.config import settable_keys
|
|
20
|
+
from ring.i18n import gettext as _
|
|
21
|
+
|
|
22
|
+
# 子命令與頂層旗標——跟 cli.main 的 dispatch / argparse 保持同步。
|
|
23
|
+
_COMMANDS = (
|
|
24
|
+
"hook",
|
|
25
|
+
"install-hooks",
|
|
26
|
+
"remove-hooks",
|
|
27
|
+
"config",
|
|
28
|
+
"focus",
|
|
29
|
+
"gc",
|
|
30
|
+
"doctor",
|
|
31
|
+
"stats",
|
|
32
|
+
"completion",
|
|
33
|
+
)
|
|
34
|
+
_TOP_FLAGS = (
|
|
35
|
+
"--watch",
|
|
36
|
+
"--interval",
|
|
37
|
+
"--count",
|
|
38
|
+
"--all",
|
|
39
|
+
"--legend",
|
|
40
|
+
"--no-legend",
|
|
41
|
+
"--lang",
|
|
42
|
+
"--format",
|
|
43
|
+
"--version",
|
|
44
|
+
"--help",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
_ZSH_TEMPLATE = """\
|
|
48
|
+
# ring zsh completion — eval "$(ring completion zsh)"
|
|
49
|
+
_ring() {{
|
|
50
|
+
local -a commands
|
|
51
|
+
commands=(
|
|
52
|
+
'hook:read a provider hook payload from stdin'
|
|
53
|
+
'install-hooks:install Claude Code / Codex hooks'
|
|
54
|
+
'remove-hooks:remove Claude Code / Codex hooks'
|
|
55
|
+
'config:show / get / set configuration'
|
|
56
|
+
'focus:focus a session by id'
|
|
57
|
+
'gc:clean stale RiNG state files'
|
|
58
|
+
'doctor:read-only environment diagnosis'
|
|
59
|
+
'stats:waiting-time statistics'
|
|
60
|
+
'completion:print shell completion script'
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
if (( CURRENT == 2 )); then
|
|
64
|
+
_describe -t commands 'ring command' commands
|
|
65
|
+
_arguments \\
|
|
66
|
+
'--watch[keep refreshing]' \\
|
|
67
|
+
'--interval[refresh seconds]:seconds:' \\
|
|
68
|
+
'--count[frames before exit]:count:' \\
|
|
69
|
+
'--all[include ended sessions]' \\
|
|
70
|
+
'--legend[show legend]' \\
|
|
71
|
+
'--no-legend[hide legend]' \\
|
|
72
|
+
'--lang[UI language]:lang:(en zh-Hant)' \\
|
|
73
|
+
'--format[output format]:format:(table json oneline)' \\
|
|
74
|
+
'--version[show version]'
|
|
75
|
+
return
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
case $words[2] in
|
|
79
|
+
hook)
|
|
80
|
+
_arguments '--provider[provider name]:provider:(claude-code codex)'
|
|
81
|
+
;;
|
|
82
|
+
install-hooks|remove-hooks)
|
|
83
|
+
_arguments '--dry-run[preview only, no writes]'
|
|
84
|
+
;;
|
|
85
|
+
gc)
|
|
86
|
+
_arguments \\
|
|
87
|
+
'--dry-run[preview only, no deletes]' \\
|
|
88
|
+
'--older-than[age threshold, e.g. 30m / 2h / 7d]:duration:' \\
|
|
89
|
+
'--all-ended[remove every ended registry]'
|
|
90
|
+
;;
|
|
91
|
+
completion)
|
|
92
|
+
(( CURRENT == 3 )) && _values 'shell' zsh bash
|
|
93
|
+
;;
|
|
94
|
+
config)
|
|
95
|
+
if (( CURRENT == 3 )); then
|
|
96
|
+
_values 'config action' get set
|
|
97
|
+
elif (( CURRENT == 4 )); then
|
|
98
|
+
_values 'config key' {keys}
|
|
99
|
+
fi
|
|
100
|
+
;;
|
|
101
|
+
esac
|
|
102
|
+
}}
|
|
103
|
+
if ! command -v compdef >/dev/null 2>&1; then
|
|
104
|
+
autoload -Uz compinit
|
|
105
|
+
compinit
|
|
106
|
+
fi
|
|
107
|
+
compdef _ring ring
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
_BASH_TEMPLATE = """\
|
|
111
|
+
# ring bash completion — eval "$(ring completion bash)"
|
|
112
|
+
_ring_completion() {{
|
|
113
|
+
local cur prev
|
|
114
|
+
cur="${{COMP_WORDS[COMP_CWORD]}}"
|
|
115
|
+
prev="${{COMP_WORDS[COMP_CWORD - 1]}}"
|
|
116
|
+
|
|
117
|
+
if [[ $COMP_CWORD -eq 1 ]]; then
|
|
118
|
+
COMPREPLY=( $(compgen -W "{commands} {flags}" -- "$cur") )
|
|
119
|
+
return
|
|
120
|
+
fi
|
|
121
|
+
|
|
122
|
+
case "$prev" in
|
|
123
|
+
--format) COMPREPLY=( $(compgen -W "table json oneline" -- "$cur") ); return ;;
|
|
124
|
+
--lang) COMPREPLY=( $(compgen -W "en zh-Hant" -- "$cur") ); return ;;
|
|
125
|
+
--provider) COMPREPLY=( $(compgen -W "claude-code codex" -- "$cur") ); return ;;
|
|
126
|
+
esac
|
|
127
|
+
|
|
128
|
+
case "${{COMP_WORDS[1]}}" in
|
|
129
|
+
hook) COMPREPLY=( $(compgen -W "--provider" -- "$cur") ) ;;
|
|
130
|
+
install-hooks|remove-hooks) COMPREPLY=( $(compgen -W "--dry-run" -- "$cur") ) ;;
|
|
131
|
+
gc) COMPREPLY=( $(compgen -W "--dry-run --older-than --all-ended" -- "$cur") ) ;;
|
|
132
|
+
completion) [[ $COMP_CWORD -eq 2 ]] && COMPREPLY=( $(compgen -W "zsh bash" -- "$cur") ) ;;
|
|
133
|
+
config)
|
|
134
|
+
if [[ $COMP_CWORD -eq 2 ]]; then
|
|
135
|
+
COMPREPLY=( $(compgen -W "get set" -- "$cur") )
|
|
136
|
+
elif [[ $COMP_CWORD -eq 3 ]]; then
|
|
137
|
+
COMPREPLY=( $(compgen -W "{keys}" -- "$cur") )
|
|
138
|
+
fi
|
|
139
|
+
;;
|
|
140
|
+
esac
|
|
141
|
+
}}
|
|
142
|
+
complete -F _ring_completion ring
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def completion_script(shell: str) -> str | None:
|
|
147
|
+
"""生成指定 shell 的補全腳本;不支援的 shell 回 ``None``。"""
|
|
148
|
+
keys = settable_keys()
|
|
149
|
+
if shell == "zsh":
|
|
150
|
+
return _ZSH_TEMPLATE.format(keys=" ".join(keys))
|
|
151
|
+
if shell == "bash":
|
|
152
|
+
return _BASH_TEMPLATE.format(commands=" ".join(_COMMANDS), flags=" ".join(_TOP_FLAGS), keys=" ".join(keys))
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def run_completion(args: list[str]) -> int:
|
|
157
|
+
"""``ring completion`` 進入點:印腳本到 stdout;shell 缺漏 / 不支援 → 用法到 stderr、rc 2。"""
|
|
158
|
+
args = _strip_lang(args)
|
|
159
|
+
if len(args) != 1:
|
|
160
|
+
print(_("用法:ring completion zsh|bash"), file=sys.stderr)
|
|
161
|
+
return 2
|
|
162
|
+
script = completion_script(args[0])
|
|
163
|
+
if script is None:
|
|
164
|
+
print(_("不支援的 shell:{shell}(目前支援 zsh / bash)", shell=args[0]), file=sys.stderr)
|
|
165
|
+
return 2
|
|
166
|
+
print(script)
|
|
167
|
+
return 0
|