ring-cli 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
ring/locale/ring.pot ADDED
@@ -0,0 +1,484 @@
1
+ # Translations template for PROJECT.
2
+ # Copyright (C) 2026 ORGANIZATION
3
+ # This file is distributed under the same license as the PROJECT project.
4
+ # FIRST AUTHOR <EMAIL@ADDRESS>, 2026.
5
+ #
6
+ #, fuzzy
7
+ msgid ""
8
+ msgstr ""
9
+ "Project-Id-Version: PROJECT VERSION\n"
10
+ "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
11
+ "POT-Creation-Date: 2026-06-25 10:35+0800\n"
12
+ "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
13
+ "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
14
+ "Language-Team: LANGUAGE <LL@li.org>\n"
15
+ "MIME-Version: 1.0\n"
16
+ "Content-Type: text/plain; charset=utf-8\n"
17
+ "Content-Transfer-Encoding: 8bit\n"
18
+ "Generated-By: Babel 2.18.0\n"
19
+
20
+ #: src/ring/cli.py:117
21
+ msgid "等你"
22
+ msgstr ""
23
+
24
+ #: src/ring/cli.py:118
25
+ msgid "工作中"
26
+ msgstr ""
27
+
28
+ #: src/ring/cli.py:119
29
+ msgid "跑完停著"
30
+ msgstr ""
31
+
32
+ #: src/ring/cli.py:120
33
+ msgid "已離場"
34
+ msgstr ""
35
+
36
+ #: src/ring/cli.py:142
37
+ #, python-brace-format
38
+ msgid "{n} 個 session 在場"
39
+ msgid_plural "{n} 個 session 在場"
40
+ msgstr[0] ""
41
+ msgstr[1] ""
42
+
43
+ #: src/ring/cli.py:143
44
+ #, python-brace-format
45
+ msgid "{n} 個 agent process 跑著"
46
+ msgid_plural "{n} 個 agent process 跑著"
47
+ msgstr[0] ""
48
+ msgstr[1] ""
49
+
50
+ #: src/ring/cli.py:144
51
+ #, python-brace-format
52
+ msgid "🎤 RiNG — {sess} · {proc}"
53
+ msgstr ""
54
+
55
+ #: src/ring/cli.py:149 src/ring/cli.py:197 src/ring/tui.py:204
56
+ msgid "圖例"
57
+ msgstr ""
58
+
59
+ #: src/ring/cli.py:161 src/ring/cli.py:199
60
+ msgid "(場館目前沒人上台)"
61
+ msgstr ""
62
+
63
+ #: src/ring/cli.py:165 src/ring/cli.py:270 src/ring/cli.py:368
64
+ #: src/ring/cli.py:470 src/ring/tui.py:163
65
+ msgid "狀態"
66
+ msgstr ""
67
+
68
+ #: src/ring/cli.py:167 src/ring/cli.py:215 src/ring/tui.py:165
69
+ msgid "工具"
70
+ msgstr ""
71
+
72
+ #: src/ring/cli.py:168 src/ring/cli.py:215 src/ring/tui.py:166
73
+ msgid "專案"
74
+ msgstr ""
75
+
76
+ #: src/ring/cli.py:169 src/ring/cli.py:215 src/ring/tui.py:166
77
+ msgid "進度"
78
+ msgstr ""
79
+
80
+ #: src/ring/cli.py:170 src/ring/cli.py:215 src/ring/tui.py:166
81
+ msgid "閒置"
82
+ msgstr ""
83
+
84
+ #: src/ring/cli.py:171 src/ring/cli.py:215 src/ring/tui.py:166
85
+ msgid "去哪"
86
+ msgstr ""
87
+
88
+ #: src/ring/cli.py:173 src/ring/cli.py:215 src/ring/tui.py:166
89
+ msgid "動作"
90
+ msgstr ""
91
+
92
+ #: src/ring/cli.py:250
93
+ msgid "(內建預設)"
94
+ msgstr ""
95
+
96
+ #: src/ring/cli.py:268
97
+ msgid "RiNG 設定檔"
98
+ msgstr ""
99
+
100
+ #: src/ring/cli.py:269 src/ring/cli.py:469
101
+ msgid "路徑"
102
+ msgstr ""
103
+
104
+ #: src/ring/cli.py:270 src/ring/cli.py:470
105
+ msgid "已存在"
106
+ msgstr ""
107
+
108
+ #: src/ring/cli.py:270 src/ring/cli.py:470
109
+ msgid "不存在(全部用內建預設)"
110
+ msgstr ""
111
+
112
+ #: src/ring/cli.py:272
113
+ msgid "目前生效的設定(← = 你覆寫過的)"
114
+ msgstr ""
115
+
116
+ #: src/ring/cli.py:280
117
+ msgid ""
118
+ "用 `ring config set KEY VALUE` 改,或直接編輯上面那個檔;完整選項見 src/ring/config.py 的 "
119
+ "docstring。"
120
+ msgstr ""
121
+
122
+ #: src/ring/cli.py:293 src/ring/cli.py:296
123
+ #, python-brace-format
124
+ msgid "未知的鍵:{key}"
125
+ msgstr ""
126
+
127
+ #: src/ring/cli.py:326
128
+ msgid "用法:ring config get KEY"
129
+ msgstr ""
130
+
131
+ #: src/ring/cli.py:337
132
+ msgid "用法:ring config set KEY VALUE"
133
+ msgstr ""
134
+
135
+ #: src/ring/cli.py:345
136
+ #, python-brace-format
137
+ msgid "✅ 已設定 {key} = {value}({path})"
138
+ msgstr ""
139
+
140
+ #: src/ring/cli.py:346
141
+ msgid "註:set 會重寫整個設定檔,原有註解不會保留。"
142
+ msgstr ""
143
+
144
+ #: src/ring/cli.py:349
145
+ #, python-brace-format
146
+ msgid "未知的 config 動作:{action}(用 get / set,或不帶參數看目前設定)"
147
+ msgstr ""
148
+
149
+ #: src/ring/cli.py:357
150
+ msgid "用法:ring doctor"
151
+ msgstr ""
152
+
153
+ #: src/ring/cli.py:367
154
+ msgid "RiNG 環境診斷"
155
+ msgstr ""
156
+
157
+ #: src/ring/cli.py:368
158
+ msgid "唯讀檢查,不會改動任何設定"
159
+ msgstr ""
160
+
161
+ #: src/ring/cli.py:372
162
+ msgid "Session 來源"
163
+ msgstr ""
164
+
165
+ #: src/ring/cli.py:382
166
+ msgid "活著"
167
+ msgstr ""
168
+
169
+ #: src/ring/cli.py:383
170
+ #, python-brace-format
171
+ msgid "偵測到 {n} 個 session"
172
+ msgstr ""
173
+
174
+ #: src/ring/cli.py:386
175
+ msgid "偵測失敗"
176
+ msgstr ""
177
+
178
+ #: src/ring/cli.py:390
179
+ msgid "Hook 安裝"
180
+ msgstr ""
181
+
182
+ #: src/ring/cli.py:400
183
+ msgid "未使用 Codex(zero-config)"
184
+ msgstr ""
185
+
186
+ #: src/ring/cli.py:402
187
+ msgid "已安裝"
188
+ msgstr ""
189
+
190
+ #: src/ring/cli.py:404
191
+ msgid "未安裝(執行 ring install-hooks)"
192
+ msgstr ""
193
+
194
+ #: src/ring/cli.py:409
195
+ msgid "通知後端"
196
+ msgstr ""
197
+
198
+ #: src/ring/cli.py:410
199
+ msgid "目前設定"
200
+ msgstr ""
201
+
202
+ #: src/ring/cli.py:417 src/ring/cli.py:451 src/ring/cli.py:460
203
+ msgid "可用"
204
+ msgstr ""
205
+
206
+ #: src/ring/cli.py:417
207
+ msgid "不可用"
208
+ msgstr ""
209
+
210
+ #: src/ring/cli.py:421 src/ring/cli.py:437
211
+ msgid "auto 實際選中"
212
+ msgstr ""
213
+
214
+ #: src/ring/cli.py:426
215
+ msgid "macOS 提醒:若只聽到聲音但沒有通知框,請到系統設定的通知項目啟用 Banner/Alert。"
216
+ msgstr ""
217
+
218
+ #: src/ring/cli.py:432
219
+ msgid "backend=none"
220
+ msgstr ""
221
+
222
+ #: src/ring/cli.py:434
223
+ msgid "agent-hooks 已接手"
224
+ msgstr ""
225
+
226
+ #: src/ring/cli.py:436
227
+ msgid "全部不可用"
228
+ msgstr ""
229
+
230
+ #: src/ring/cli.py:437
231
+ msgid "不發通知"
232
+ msgstr ""
233
+
234
+ #: src/ring/cli.py:441
235
+ msgid "聚焦終端(focuser)"
236
+ msgstr ""
237
+
238
+ #: src/ring/cli.py:451
239
+ msgid "不可用(tmux 不在 PATH)"
240
+ msgstr ""
241
+
242
+ #: src/ring/cli.py:455
243
+ msgid "不可用(osascript 不在 PATH)"
244
+ msgstr ""
245
+
246
+ #: src/ring/cli.py:460 src/ring/cli.py:462
247
+ msgid "不可用(app 沒在跑)"
248
+ msgstr ""
249
+
250
+ #: src/ring/cli.py:467
251
+ msgid "設定檔"
252
+ msgstr ""
253
+
254
+ #: src/ring/cli.py:471
255
+ msgid "完整生效值請看 `ring config`。"
256
+ msgstr ""
257
+
258
+ #: src/ring/cli.py:482
259
+ #, python-brace-format
260
+ msgid "每 {interval}s 刷新 · Ctrl-C 離場"
261
+ msgstr ""
262
+
263
+ #: src/ring/cli.py:541
264
+ msgid ""
265
+ "\n"
266
+ "commands:\n"
267
+ " hook [PROVIDER] 從 stdin 讀 provider hook payload,寫入 RiNG "
268
+ "registry\n"
269
+ " hook --provider PROVIDER 同上,明確指定 provider(例如 codex)\n"
270
+ " install-hooks [--dry-run] 安裝 Claude Code / Codex hooks\n"
271
+ " remove-hooks [--dry-run] 移除 Claude Code / Codex hooks\n"
272
+ " config 顯示設定檔位置與目前生效的設定\n"
273
+ " config get KEY 讀單一設定的目前值\n"
274
+ " config set KEY VALUE 寫入單一設定(會重寫設定檔,不保留註解)\n"
275
+ " focus SESSION_ID 聚焦指定 session;TUI 在跑時會回到 RiNG 並選中該列\n"
276
+ " doctor 顯示環境診斷(唯讀)——hook 安裝狀態、通知後端、focuser 可用性\n"
277
+ msgstr ""
278
+
279
+ #: src/ring/cli.py:559
280
+ msgid ""
281
+ "usage: ring hook [PROVIDER] [--provider PROVIDER]\n"
282
+ "\n"
283
+ "從 stdin 讀 hook JSON,依 provider 正規化後寫入 RiNG registry。\n"
284
+ msgstr ""
285
+
286
+ #: src/ring/cli.py:565
287
+ msgid ""
288
+ "usage: ring install-hooks [--dry-run]\n"
289
+ "\n"
290
+ "安裝 Claude Code / Codex hooks。\n"
291
+ msgstr ""
292
+
293
+ #: src/ring/cli.py:571
294
+ msgid ""
295
+ "usage: ring remove-hooks [--dry-run]\n"
296
+ "\n"
297
+ "從 Claude Code / Codex hook 設定移除 RiNG 安裝的 hooks。\n"
298
+ msgstr ""
299
+
300
+ #: src/ring/cli.py:577
301
+ msgid ""
302
+ "usage: ring config [get KEY | set KEY VALUE]\n"
303
+ "\n"
304
+ "不帶參數:顯示設定檔位置(~/.config/ring/config.toml)與目前生效的所有設定。\n"
305
+ " get KEY 印出單一設定的目前值(colors 子鍵用 colors.<name>)。\n"
306
+ " set KEY VALUE 寫入單一設定。注意:會重寫整個設定檔,原有註解不會保留。\n"
307
+ msgstr ""
308
+
309
+ #: src/ring/cli.py:585
310
+ msgid ""
311
+ "usage: ring focus SESSION_ID\n"
312
+ "\n"
313
+ "聚焦指定 session;若 RiNG TUI 正在執行,會回到 TUI 並選中該列。\n"
314
+ msgstr ""
315
+
316
+ #: src/ring/cli.py:591
317
+ msgid ""
318
+ "usage: ring doctor\n"
319
+ "\n"
320
+ "唯讀環境診斷:逐節報告 hook 安裝狀態、通知後端可用性、focuser 可用性與設定檔位置。\n"
321
+ "不寫任何檔案、不安裝、不發通知;固定回傳 0。\n"
322
+ msgstr ""
323
+
324
+ #: src/ring/cli.py:656
325
+ msgid "看所有 agent CLI session 上台。"
326
+ msgstr ""
327
+
328
+ #: src/ring/cli.py:661
329
+ msgid "持續刷新"
330
+ msgstr ""
331
+
332
+ #: src/ring/cli.py:662
333
+ msgid "watch 刷新秒數"
334
+ msgstr ""
335
+
336
+ #: src/ring/cli.py:663
337
+ msgid "watch 刷新幾格後自動結束(0=無限,預設)"
338
+ msgstr ""
339
+
340
+ #: src/ring/cli.py:664
341
+ msgid "連已離場的 session 也顯示"
342
+ msgstr ""
343
+
344
+ #: src/ring/cli.py:669
345
+ msgid "顯示顏色圖例(--no-legend 關閉)"
346
+ msgstr ""
347
+
348
+ #: src/ring/cli.py:671
349
+ msgid "語言(如 en / zh-Hant;也吃 config / RING_LANG / LANG)"
350
+ msgstr ""
351
+
352
+ #: src/ring/hook.py:73
353
+ msgid "重開 Claude Code session 後,🔴 等你狀態就會精準起來。"
354
+ msgstr ""
355
+
356
+ #: src/ring/hook.py:83
357
+ msgid "重開 Codex session、並在它詢問時「信任」這個 hook,🔴 等你狀態才會精準起來(Codex 不會執行未信任的 hook)。"
358
+ msgstr ""
359
+
360
+ #: src/ring/hook.py:331
361
+ #, python-brace-format
362
+ msgid "⚠️ 偵測到其他工具也掛在 {events}:{cmds}"
363
+ msgstr ""
364
+
365
+ #: src/ring/hook.py:332
366
+ msgid "它們會跟 RiNG 的通知重複觸發;要完全改用 RiNG,建議移除它們。"
367
+ msgstr ""
368
+
369
+ #: src/ring/hook.py:380 src/ring/hook.py:456
370
+ #, python-brace-format
371
+ msgid "⚠️ {path} 不是合法 JSON,先處理它再來。"
372
+ msgstr ""
373
+
374
+ #: src/ring/hook.py:418
375
+ #, python-brace-format
376
+ msgid "✅ RiNG hooks 已經裝過了,沒有變更。({path})"
377
+ msgstr ""
378
+
379
+ #: src/ring/hook.py:431
380
+ #, python-brace-format
381
+ msgid "✅ 已註冊 RiNG hooks({events})到 {path}"
382
+ msgstr ""
383
+
384
+ #: src/ring/hook.py:450
385
+ #, python-brace-format
386
+ msgid "ℹ️ {path} 不存在,沒有可移除的 hook。"
387
+ msgstr ""
388
+
389
+ #: src/ring/hook.py:477
390
+ #, python-brace-format
391
+ msgid "ℹ️ 沒有找到 RiNG hook 條目,無需移除。({path})"
392
+ msgstr ""
393
+
394
+ #: src/ring/hook.py:481
395
+ #, python-brace-format
396
+ msgid "✅ 已移除 RiNG hooks({events})從 {path}"
397
+ msgstr ""
398
+
399
+ #: src/ring/tui.py:88
400
+ #, python-brace-format
401
+ msgid "為 {project} 命名(Enter 存、Esc 取消、清空移除)"
402
+ msgstr ""
403
+
404
+ #: src/ring/tui.py:89
405
+ msgid "這個 session 在做什麼…"
406
+ msgstr ""
407
+
408
+ #: src/ring/tui.py:106
409
+ msgid "離場"
410
+ msgstr ""
411
+
412
+ #: src/ring/tui.py:107
413
+ msgid "刷新"
414
+ msgstr ""
415
+
416
+ #: src/ring/tui.py:108
417
+ msgid "含已離場"
418
+ msgstr ""
419
+
420
+ #: src/ring/tui.py:109
421
+ msgid "命名"
422
+ msgstr ""
423
+
424
+ #: src/ring/tui.py:110 src/ring/tui.py:111
425
+ msgid "跳過去"
426
+ msgstr ""
427
+
428
+ #: src/ring/tui.py:184
429
+ msgid "💡 同專案開了多個 session,裝 hook 跳轉才精準:ring install-hooks"
430
+ msgstr ""
431
+
432
+ #: src/ring/tui.py:186
433
+ msgid "↑/↓ 選一列,Enter 或 Space 跳到那個 session 的終端"
434
+ msgstr ""
435
+
436
+ #: src/ring/tui.py:214
437
+ #, python-brace-format
438
+ msgid "🔔 {names} 在等你回話"
439
+ msgstr ""
440
+
441
+ #: src/ring/tui.py:255
442
+ #, python-brace-format
443
+ msgid "→ 已跳到 {project}(來自通知)"
444
+ msgstr ""
445
+
446
+ #: src/ring/tui.py:260
447
+ msgid "那個 session 已不在場"
448
+ msgstr ""
449
+
450
+ #: src/ring/tui.py:324 src/ring/tui.py:345
451
+ msgid "(沒有選到 session)"
452
+ msgstr ""
453
+
454
+ #: src/ring/tui.py:351
455
+ #, python-brace-format
456
+ msgid "→ {project}({where})"
457
+ msgstr ""
458
+
459
+ #: src/ring/tui.py:353
460
+ #, python-brace-format
461
+ msgid "{project}:{msg}"
462
+ msgstr ""
463
+
464
+ #: src/ring/focus/__init__.py:48
465
+ msgid "已離場的 session 無法跳轉"
466
+ msgstr ""
467
+
468
+ #: src/ring/focus/__init__.py:61
469
+ msgid "找不到這個終端分頁(可能已關閉;刷新後若仍存在,請裝 hook 取得更精準狀態)"
470
+ msgstr ""
471
+
472
+ #: src/ring/focus/__init__.py:60
473
+ msgid "沒有 focuser 接得住(裝 hook,或一個專案只開一個 session 才測得到 tty)"
474
+ msgstr ""
475
+
476
+ #: src/ring/notify/__init__.py:112
477
+ msgid "💡 裝 terminal-notifier 可點擊通知直接跳回 session:brew install terminal-notifier"
478
+ msgstr ""
479
+
480
+ #: src/ring/notify/notify_send.py:22 src/ring/notify/osascript_notifier.py:24
481
+ #: src/ring/notify/terminal_notifier.py:48
482
+ #, python-brace-format
483
+ msgid "RiNG · {project} 在等你回話"
484
+ msgstr ""
@@ -0,0 +1,120 @@
1
+ """系統通知 package——把「新轉為等你」的 session 發給使用者。
2
+
3
+ 可插拔的 ``Notifier`` 抽象層(跟 ``focus`` 的 focuser、``sources`` 的 source 同一套
4
+ 設計):core 不認識任何具體後端,只依序問每個已註冊 notifier「你能用嗎」。要支援新
5
+ 平台=在這個 package 加一個模組、``register_notifier()`` 註冊,core 零改動。內建:
6
+
7
+ - ``terminal-notifier``(macOS,支援點擊 ``ring focus`` 跳轉)
8
+ - ``osascript``(macOS,純文字,點擊不可聚焦——terminal-notifier 被系統擋掉時的退路)
9
+ - ``notify-send``(Linux / libnotify,純文字)
10
+
11
+ 選哪個由 config ``notify_backend`` 決定:``"auto"`` = 第一個「可用」的(優先支援點擊的),
12
+ 或指定某個 notifier 名稱強制使用。通知失敗一律安靜吞掉——通知是錦上添花,絕不打斷主流程。
13
+ 外部 binary(terminal-notifier / notify-send)不進 pyproject dependencies。
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import shutil
19
+ import sys
20
+ from pathlib import Path
21
+
22
+ from ring.config import get_config
23
+ from ring.i18n import gettext as _
24
+ from ring.notify.base import Notifier
25
+ from ring.notify.notify_send import notifier as _notify_send
26
+ from ring.notify.osascript_notifier import notifier as _osascript
27
+ from ring.notify.terminal_notifier import notifier as _terminal_notifier
28
+ from ring.registry import Session
29
+
30
+ # 一次性安裝引導的 marker 檔路徑。
31
+ _HINT_MARKER: Path = Path.home() / ".config" / "ring" / ".tn-hint-shown"
32
+
33
+ # 內建後端。順序即 auto 的偏好順序(前面優先)。terminal-notifier 先於 osascript(macOS),
34
+ # notify-send 殿後(Linux)。register_notifier(first=True) 可插到最前。
35
+ _NOTIFIERS: list[Notifier] = [_terminal_notifier, _osascript, _notify_send]
36
+
37
+
38
+ def register_notifier(notifier: Notifier, *, first: bool = False) -> None:
39
+ """外部擴充入口:註冊一個自訂通知後端(first=True 插到最前、優先嘗試)。"""
40
+ if first:
41
+ _NOTIFIERS.insert(0, notifier)
42
+ else:
43
+ _NOTIFIERS.append(notifier)
44
+
45
+
46
+ def notifiers() -> list[Notifier]:
47
+ """目前已註冊的 notifier,順序即 auto 的偏好順序。"""
48
+ return list(_NOTIFIERS)
49
+
50
+
51
+ def _select_notifier(backend: str) -> Notifier | None:
52
+ """依 config 的 notify_backend 選一個可用 notifier。
53
+
54
+ - ``"none"`` → ``None``,明確關閉通知(RiNG 當純看板用,不發任何 toast)。
55
+ - ``"agent-hooks"`` → 決策與提醒交給 agent-hooks(由 ``ring hook`` 同步出 modal)。
56
+ agent-hooks 在 PATH 上時回 ``None``(watch 不重複發 toast);不在時退回 auto,
57
+ 讓 RiNG 自己通知——所以「沒裝 agent-hooks 也不會兩頭落空」是自動保證的。
58
+ - 指定名稱且該 notifier 可用 → 用它。
59
+ - ``"auto"``(或指定的後端不存在/不可用)→ 取第一個可用的,優先支援點擊跳轉的。
60
+ - 都不可用 → ``None``(不發、不崩)。
61
+ """
62
+ if backend == "none":
63
+ return None
64
+ if backend == "agent-hooks":
65
+ if shutil.which("agent-hooks") is not None:
66
+ return None # agent-hooks 從 ring hook 那邊出 modal,watch 不重複發
67
+ backend = "auto" # 沒裝 → 退回 auto,RiNG 自己通知,不會瞎
68
+ available = [n for n in _NOTIFIERS if n.available()]
69
+ if backend != "auto":
70
+ for n in available:
71
+ if n.name == backend:
72
+ return n
73
+ # 指定的後端不可用 → 退回 auto 選法
74
+ if not available:
75
+ return None
76
+ clickable = [n for n in available if n.supports_click()]
77
+ return clickable[0] if clickable else available[0]
78
+
79
+
80
+ def notify_waiting(sessions: list[Session]) -> str | None:
81
+ """對一批「新轉為等你」的 session 發系統通知。
82
+
83
+ 依 config ``notify_backend`` 選一個 notifier 後端發送(見 ``_select_notifier``)。
84
+ auto 模式下若只選到不支援點擊的後端、且在 macOS 上,回傳一次性的安裝引導字串
85
+ (建議裝 terminal-notifier 取得點擊跳轉);其餘情況回 ``None``。失敗一律安靜吞掉。
86
+
87
+ :param sessions: 新轉為 waiting 的 session 清單;空清單時直接回傳。
88
+ :returns: 安裝引導提示字串或 ``None``。
89
+ """
90
+ if not sessions:
91
+ return None
92
+
93
+ backend = get_config().notify_backend
94
+ notifier = _select_notifier(backend)
95
+ if notifier is None:
96
+ return None
97
+ notifier.send(sessions)
98
+ # auto 落到非點擊後端、且在 macOS(terminal-notifier 是可裝的點擊選項)→ 提示一次。
99
+ if backend == "auto" and not notifier.supports_click() and sys.platform == "darwin":
100
+ return _maybe_show_install_hint()
101
+ return None
102
+
103
+
104
+ def _maybe_show_install_hint() -> str | None:
105
+ """首次走 osascript 路徑時,回傳 terminal-notifier 安裝建議字串(marker 檔防重複)。
106
+
107
+ :returns: hint 字串(首次)或 ``None``(已提示過或寫 marker 失敗時)。
108
+ """
109
+ if _HINT_MARKER.exists():
110
+ return None
111
+ try:
112
+ hint = _("💡 裝 terminal-notifier 可點擊通知直接跳回 session:brew install terminal-notifier")
113
+ _HINT_MARKER.parent.mkdir(parents=True, exist_ok=True)
114
+ _HINT_MARKER.touch()
115
+ return hint
116
+ except Exception:
117
+ return None
118
+
119
+
120
+ __all__ = ["Notifier", "notifiers", "notify_waiting", "register_notifier"]
ring/notify/base.py ADDED
@@ -0,0 +1,37 @@
1
+ """Notifier 協定 + 所有後端共用的純 helper(只放契約與共用文字,不放具體後端)。
2
+
3
+ core 只透過 ``Notifier`` 介面跟具體平台溝通;要支援新平台=在這個 package 裡新增一個
4
+ 模組、定義一個 Notifier 並 ``register_notifier()`` 註冊,core 零改動。
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Protocol
10
+
11
+ from ring.registry import Session
12
+
13
+
14
+ class Notifier(Protocol):
15
+ """一個系統通知後端。core 只透過這個介面跟具體平台溝通。"""
16
+
17
+ name: str
18
+
19
+ def available(self) -> bool:
20
+ """這個後端在當前系統能不能用(通常是對應 binary 是否存在)。"""
21
+ ...
22
+
23
+ def supports_click(self) -> bool:
24
+ """點擊通知能不能觸發 ``ring focus`` 跳轉。"""
25
+ ...
26
+
27
+ def send(self, sessions: list[Session]) -> None:
28
+ """逐 session 各發一則通知;失敗安靜吞掉。"""
29
+ ...
30
+
31
+
32
+ def notify_message(session: Session) -> str:
33
+ """通知內文——只給重要訊息:去哪(完整路徑或 tmux 座標)。
34
+
35
+ 標題已經說了「哪個專案在等你」,內文就補「位置」讓你(尤其不能點擊跳轉時)知道去哪。
36
+ """
37
+ return f"📍 {session.location}"
ring/notify/command.py ADDED
@@ -0,0 +1,18 @@
1
+ """以「對應 CLI binary 在不在 PATH」判定 available 的 notifier 共用基底。
2
+
3
+ 內建三個後端(terminal-notifier / osascript / notify-send)的 binary 名剛好等於 notifier
4
+ 名,所以共用這段;binary 名與 notifier 名不同的後端覆寫 ``available()`` 即可。子類別仍要
5
+ 自己定義 ``name`` 與 ``supports_click()`` / ``send()``——跟 ``focus.applescript`` 把共用實作
6
+ 拆出 ``base`` 同一個道理。
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import shutil
12
+
13
+
14
+ class CommandNotifier:
15
+ name: str
16
+
17
+ def available(self) -> bool:
18
+ return shutil.which(self.name) is not None
@@ -0,0 +1,30 @@
1
+ """notify-send 後端(Linux / libnotify 純文字,點擊不可聚焦)。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+
7
+ from ring.i18n import gettext as _
8
+ from ring.notify.base import notify_message
9
+ from ring.notify.command import CommandNotifier
10
+ from ring.registry import Session
11
+
12
+
13
+ class NotifySendNotifier(CommandNotifier):
14
+ name = "notify-send"
15
+
16
+ def supports_click(self) -> bool:
17
+ return False
18
+
19
+ def send(self, sessions: list[Session]) -> None:
20
+ """用 libnotify 的 ``notify-send`` 逐 session 各發一則純文字通知。"""
21
+ for s in sessions:
22
+ title = _("RiNG · {project} 在等你回話", project=s.project)
23
+ message = notify_message(s)
24
+ try:
25
+ subprocess.run(["notify-send", title, message], capture_output=True, timeout=10)
26
+ except Exception:
27
+ pass
28
+
29
+
30
+ notifier = NotifySendNotifier()