claude-code-tools 1.19.6__tar.gz → 1.19.7__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.
Files changed (126) hide show
  1. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/PKG-INFO +1 -1
  2. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/__init__.py +1 -1
  3. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/voice_type/app.py +5 -1
  4. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/voice_type/config.py +25 -5
  5. claude_code_tools-1.19.7/claude_code_tools/voice_type/overlay.py +402 -0
  6. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/pyproject.toml +2 -2
  7. claude_code_tools-1.19.6/claude_code_tools/voice_type/overlay.py +0 -273
  8. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/.gitignore +0 -0
  9. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/LICENSE +0 -0
  10. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/README.md +0 -0
  11. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/action_rpc.py +0 -0
  12. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/agent_tunnel/__init__.py +0 -0
  13. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/agent_tunnel/backends.py +0 -0
  14. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/agent_tunnel/cli.py +0 -0
  15. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/agent_tunnel/config.py +0 -0
  16. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/agent_tunnel/convert.py +0 -0
  17. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/agent_tunnel/discord_bot.py +0 -0
  18. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/agent_tunnel/locking.py +0 -0
  19. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/agent_tunnel/paths.py +0 -0
  20. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/agent_tunnel/registry.py +0 -0
  21. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/agent_tunnel/session.py +0 -0
  22. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/agent_tunnel/store.py +0 -0
  23. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/agent_tunnel/tmux.py +0 -0
  24. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/agent_tunnel/trust.py +0 -0
  25. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/aichat.py +0 -0
  26. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/claude_continue.py +0 -0
  27. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/codex_continue.py +0 -0
  28. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/codex_server.py +0 -0
  29. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/codex_server_cli.py +0 -0
  30. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/codex_server_fingerprint.py +0 -0
  31. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/codex_server_generation.py +0 -0
  32. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/codex_server_legacy.py +0 -0
  33. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/codex_server_models.py +0 -0
  34. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/codex_server_process.py +0 -0
  35. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/codex_server_reservation.py +0 -0
  36. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/codex_server_retry.py +0 -0
  37. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/codex_server_reuse.py +0 -0
  38. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/codex_server_state.py +0 -0
  39. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/codex_server_supervisor.py +0 -0
  40. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/codex_server_worker.py +0 -0
  41. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/config.py +0 -0
  42. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/csv2gsheet.py +0 -0
  43. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/delete_session.py +0 -0
  44. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/dotenv_vault.py +0 -0
  45. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/env_safe.py +0 -0
  46. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/export_all.py +0 -0
  47. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/export_claude_session.py +0 -0
  48. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/export_codex_session.py +0 -0
  49. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/export_session.py +0 -0
  50. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/find_claude_session.py +0 -0
  51. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/find_codex_session.py +0 -0
  52. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/find_original_session.py +0 -0
  53. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/find_session.py +0 -0
  54. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/find_trimmed_sessions.py +0 -0
  55. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/fix_session.py +0 -0
  56. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/gdoc2docx.py +0 -0
  57. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/gdoc2md.py +0 -0
  58. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/gsheet2csv.py +0 -0
  59. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/md2gdoc.py +0 -0
  60. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/msg/__init__.py +0 -0
  61. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/msg/cli.py +0 -0
  62. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/msg/hooks.py +0 -0
  63. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/msg/models.py +0 -0
  64. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/msg/prompt_detect.py +0 -0
  65. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/msg/store.py +0 -0
  66. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/msg/watcher.py +0 -0
  67. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/node_menu_ui.py +0 -0
  68. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/port_claude_noise.py +0 -0
  69. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/port_claude_to_codex.py +0 -0
  70. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/port_codex_flatten.py +0 -0
  71. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/port_codex_to_claude.py +0 -0
  72. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/port_service.py +0 -0
  73. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/resolve_session.py +0 -0
  74. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/resolve_session_names.py +0 -0
  75. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/resolve_session_render.py +0 -0
  76. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/search_index.py +0 -0
  77. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/session_lineage.py +0 -0
  78. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/session_menu.py +0 -0
  79. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/session_menu_cli.py +0 -0
  80. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/session_utils.py +0 -0
  81. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/smart_trim.py +0 -0
  82. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/smart_trim_core.py +0 -0
  83. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/tmux_cli_controller.py +0 -0
  84. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/tmux_execution_helpers.py +0 -0
  85. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/tmux_remote_controller.py +0 -0
  86. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/trim_in_place.py +0 -0
  87. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/trim_session.py +0 -0
  88. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/trim_session_claude.py +0 -0
  89. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/trim_session_codex.py +0 -0
  90. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/voice_type/__init__.py +0 -0
  91. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/voice_type/cli.py +0 -0
  92. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/voice_type/engine_parakeet.py +0 -0
  93. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/voice_type/engine_parakeet_mlx.py +0 -0
  94. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/voice_type/engines.py +0 -0
  95. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/voice_type/hotkey.py +0 -0
  96. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/voice_type/inject.py +0 -0
  97. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/voice_type/logic.py +0 -0
  98. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/workflow_cli.py +0 -0
  99. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/workflow_cli_contract.py +0 -0
  100. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/workflow_cli_formatting.py +0 -0
  101. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/workflow_cli_identity_policy.py +0 -0
  102. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/workflow_cli_manifest.py +0 -0
  103. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/workflow_cli_projection.py +0 -0
  104. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/workflow_cli_rendering.py +0 -0
  105. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/workflow_cli_snapshots.py +0 -0
  106. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/workflow_cli_store_backends.py +0 -0
  107. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/workflow_processes.py +0 -0
  108. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/workflow_runs.py +0 -0
  109. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/workflow_store_io.py +0 -0
  110. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/claude_code_tools/workflow_validation.py +0 -0
  111. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/docs/agent-tunnel-spec.md +0 -0
  112. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/docs/claude-code-tmux-tutorials.md +0 -0
  113. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/docs/dot-zshrc.md +0 -0
  114. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/docs/find-claude-session.md +0 -0
  115. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/docs/intercom-spec.md +0 -0
  116. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/docs/lmsh.md +0 -0
  117. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/docs/local-llm-setup.md +0 -0
  118. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/docs/msg-phase-a-plan.md +0 -0
  119. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/docs/msg-plan-v2.md +0 -0
  120. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/docs/tmux-cli-instructions.md +0 -0
  121. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/docs/vault-documentation.md +0 -0
  122. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/hatch_build.py +0 -0
  123. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/node_ui/action_config.js +0 -0
  124. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/node_ui/menu.js +0 -0
  125. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/node_ui/package-lock.json +0 -0
  126. {claude_code_tools-1.19.6 → claude_code_tools-1.19.7}/node_ui/package.json +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: claude-code-tools
3
- Version: 1.19.6
3
+ Version: 1.19.7
4
4
  Summary: Collection of tools for working with Claude Code
5
5
  License-File: LICENSE
6
6
  Requires-Python: >=3.11
@@ -1,3 +1,3 @@
1
1
  """Claude Code Tools - Collection of utilities for Claude Code."""
2
2
 
3
- __version__ = "1.19.6"
3
+ __version__ = "1.19.7"
@@ -536,7 +536,11 @@ class VoiceTypeApp:
536
536
  return state.value, state is State.ACTIVE, level
537
537
 
538
538
  overlay.run_overlay(
539
- sample, tick, self._stop.is_set
539
+ sample,
540
+ tick,
541
+ self._stop.is_set,
542
+ flex=self.cfg.overlay_flex,
543
+ speed=self.cfg.overlay_speed,
540
544
  )
541
545
  return result["code"]
542
546
  self._status("overlay unavailable; running without it")
@@ -55,9 +55,13 @@ class Config:
55
55
  fastest for v3-int8 on Apple Silicon; 8 helps v2-fp16).
56
56
  strip_fillers: Drop standalone filler words (uh, um, ...) from
57
57
  typed text.
58
- overlay: Show the floating waveform pill while recording
59
- (macOS only; ignored elsewhere). The pill is hidden while
58
+ overlay: Show the floating ghost indicator while recording
59
+ (macOS only; ignored elsewhere). The ghost is hidden while
60
60
  paused/passive — its presence IS the recording indicator.
61
+ overlay_flex: How much the ghost's face flexes/wobbles its
62
+ shape (1.0 = default; higher = more flexible, calmer < 1.0).
63
+ overlay_speed: Overall animation speed multiplier (1.0 =
64
+ default; lower = slower/gentler motion).
61
65
  model_arch: Moonshine model architecture name (moonshine engine
62
66
  only).
63
67
  language: Language tag understood by Moonshine (e.g. "en").
@@ -98,6 +102,8 @@ class Config:
98
102
  mlx_model: str = "mlx-community/parakeet-tdt-0.6b-v3"
99
103
  strip_fillers: bool = True
100
104
  overlay: bool = True
105
+ overlay_flex: float = 1.0
106
+ overlay_speed: float = 1.0
101
107
  model_arch: str = "medium-streaming"
102
108
  language: str = "en"
103
109
  hotkey: str = "<ctrl>+;"
@@ -181,6 +187,16 @@ class Config:
181
187
  raise ValueError(
182
188
  f"{name} must be a string, got {value!r}"
183
189
  )
190
+ for name in ("overlay_flex", "overlay_speed"):
191
+ value = getattr(self, name)
192
+ if isinstance(value, bool) or not isinstance(
193
+ value, (int, float)
194
+ ):
195
+ raise ValueError(f"{name} must be a number, got {value!r}")
196
+ if not 0.0 <= value <= 5.0:
197
+ raise ValueError(
198
+ f"{name} must be between 0 and 5, got {value!r}"
199
+ )
184
200
  if self.mode == "wake" and not self.wake_word.strip():
185
201
  raise ValueError('mode "wake" requires a non-empty wake_word')
186
202
  if isinstance(self.idle_timeout, bool) or not isinstance(
@@ -304,10 +320,14 @@ parakeet_model = "v3-int8"
304
320
  # Apple Silicon (~32x realtime); v2-fp16 benefits from 8 (~16x).
305
321
  parakeet_threads = 4
306
322
 
307
- # Floating waveform pill (macOS): shown ONLY while recording — red
308
- # waves as you speak; hidden when paused/waiting. Click-through; never
309
- # steals focus.
323
+ # Floating ghost indicator (macOS): shown ONLY while recording — a
324
+ # little blue ghost whose mouth opens with your voice; hidden when
325
+ # paused/waiting. Click-through; never steals focus.
310
326
  overlay = true
327
+ # How much the ghost's face flexes its shape (higher = more flexible).
328
+ overlay_flex = 1.0
329
+ # Overall animation speed (lower = slower, gentler motion).
330
+ overlay_speed = 1.0
311
331
 
312
332
  # Remove standalone filler words (uh, um, ...) from typed text.
313
333
  strip_fillers = true
@@ -0,0 +1,402 @@
1
+ """Floating waveform pill: visible proof that voice-type is listening.
2
+
3
+ A small always-on-top, click-through, non-activating macOS panel at the
4
+ bottom-center of the screen, shown ONLY while recording. It scrolls a
5
+ live waveform of the mic level — red waves as you speak — and hides
6
+ when paused or waiting for the wake word. Because the panel is a
7
+ non-activating borderless NSPanel that ignores mouse events, it can
8
+ never steal keyboard focus from the app being dictated into.
9
+
10
+ Uses pyobjc (already a transitive dependency of pynput on macOS).
11
+ The overlay must run on the process's MAIN thread (AppKit rule); the
12
+ app hands its per-tick housekeeping in as a callback.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import signal
18
+ from typing import Callable
19
+
20
+ # (state_label, is_recording, level 0..1) sampled ~20x per second.
21
+ SampleFn = Callable[[], tuple[str, bool, float]]
22
+ TickFn = Callable[[], None]
23
+
24
+ _WIDTH = 210.0
25
+ _HEIGHT = 240.0 # generous headroom so the halo never clips when moving
26
+ _MARGIN_BOTTOM = 110.0
27
+ _TICK_SECONDS = 0.033 # ~30 fps
28
+ # Master animation speed: small = slow, gentle, gradual motion.
29
+ _PHASE_SPEED = 0.05
30
+ # Idle blink: every ~_BLINK_EVERY frames the eyes close for a few.
31
+ _BLINK_EVERY = 130
32
+ _BLINK_FRAMES = 6
33
+
34
+
35
+ def overlay_available() -> bool:
36
+ """True if the AppKit bridge is importable (macOS with pyobjc)."""
37
+ try:
38
+ import AppKit # noqa: F401
39
+
40
+ return True
41
+ except Exception:
42
+ return False
43
+
44
+
45
+ def run_overlay( # noqa: PLR0913
46
+ sample: SampleFn,
47
+ tick: TickFn,
48
+ stopped: Callable[[], bool],
49
+ flex: float = 1.0,
50
+ speed: float = 1.0,
51
+ ) -> None:
52
+ """Show the ghost and block until ``stopped()`` returns True.
53
+
54
+ Args:
55
+ sample: Returns (state_label, is_recording, level) each frame.
56
+ tick: App housekeeping to run each frame (idle timeout, fatal
57
+ checks); exceptions are swallowed so the UI never dies.
58
+ stopped: Polled each frame; True ends the loop and closes the
59
+ panel.
60
+ flex: Face shape-flex multiplier (config overlay_flex).
61
+ speed: Animation speed multiplier (config overlay_speed).
62
+
63
+ SIGINT is redirected to a flag-friendly handler while the loop
64
+ runs (AppKit's run loop would otherwise swallow Ctrl+C), and
65
+ restored afterwards.
66
+ """
67
+ import AppKit
68
+ import objc
69
+ from AppKit import (
70
+ NSAffineTransform,
71
+ NSApplication,
72
+ NSBackingStoreBuffered,
73
+ NSBezierPath,
74
+ NSColor,
75
+ NSGradient,
76
+ NSGraphicsContext,
77
+ NSMakeRect,
78
+ NSPanel,
79
+ NSScreen,
80
+ NSTimer,
81
+ )
82
+
83
+ # Plain helpers (NOT NSView methods — pyobjc would try to bridge
84
+ # underscored methods as selectors and reject the extra arguments).
85
+ def _mk_oval(cx, cy, w, h): # noqa: ANN001, ANN202
86
+ return NSBezierPath.bezierPathWithOvalInRect_(
87
+ NSMakeRect(cx - w / 2, cy - h / 2, w, h)
88
+ )
89
+
90
+ def _mk_ghost(cx, cy, gw, jelly, ph): # noqa: ANN001, ANN202
91
+ """Ghost silhouette (domed head, wavy hem) with a soft jelly flex.
92
+
93
+ Built from points so a gentle, slow, spatially-coherent
94
+ displacement can wobble the whole outline — the head is
95
+ non-rigid (flexes shape), but the amplitude is small and smooth
96
+ so it never turns into a lumpy blob. ``jelly`` is the wobble
97
+ amplitude in points; ``ph`` the slow master phase.
98
+ """
99
+ import math
100
+
101
+ dome_y = cy + gw * 0.55
102
+ hem_y = cy - gw * 0.95
103
+ pts = [(cx + gw, hem_y), (cx + gw, dome_y)]
104
+ nd = 26
105
+ for i in range(1, nd):
106
+ a = math.pi * i / nd
107
+ pts.append((cx + gw * math.cos(a), dome_y + gw * math.sin(a)))
108
+ pts.append((cx - gw, dome_y))
109
+ pts.append((cx - gw, hem_y))
110
+ segs, bumps, ha = 30, 3, gw * 0.16
111
+ for i in range(1, segs + 1):
112
+ u = i / segs
113
+ x = cx - gw + u * 2 * gw
114
+ pts.append((x, hem_y - ha * math.sin(u * math.pi * bumps)))
115
+
116
+ p = NSBezierPath.bezierPath()
117
+ for i, (x, y) in enumerate(pts):
118
+ dx = jelly * math.sin(0.028 * y + ph * 0.8)
119
+ dy = jelly * math.sin(0.028 * x - ph * 0.6)
120
+ pt = (x + dx, y + dy)
121
+ if i == 0:
122
+ p.moveToPoint_(pt)
123
+ else:
124
+ p.lineToPoint_(pt)
125
+ p.closePath()
126
+ return p
127
+
128
+ def _mk_mouth(mx, lip_y, gw, open_, ph): # noqa: ANN001, ANN202
129
+ """An organic mouth-hole that opens downward (jaw) and morphs.
130
+
131
+ Not a scaling oval: the rim is perturbed by slow low harmonics
132
+ so it wobbles/changes shape, it opens mostly DOWNWARD from a
133
+ fixed upper-lip line (like a dropping jaw), and it stays a thin
134
+ slit at rest. ``open_`` is 0..1; ``ph`` is the slow master phase.
135
+ """
136
+ import math
137
+
138
+ rx = gw * (0.36 + 0.08 * open_)
139
+ ry = gw * (0.05 + 0.34 * open_)
140
+ cy = lip_y - ry * 0.6 # grows downward from the lip line
141
+ # Constrained flex: two gentle low harmonics at small amplitude
142
+ # so the mouth has organic life without becoming a lumpy blob.
143
+ # (It is also clipped to the face by the caller, so it can never
144
+ # spill past the ghost's silhouette.)
145
+ deform = 0.04 + 0.06 * open_
146
+ n = 44
147
+ p = NSBezierPath.bezierPath()
148
+ for i in range(n + 1):
149
+ a = 2.0 * math.pi * i / n
150
+ wob = 1.0 + deform * (
151
+ math.sin(2 * a + ph * 0.5)
152
+ + 0.4 * math.sin(3 * a - ph * 0.4)
153
+ )
154
+ x = mx + rx * wob * math.cos(a)
155
+ y = cy + ry * wob * math.sin(a)
156
+ if i == 0:
157
+ p.moveToPoint_((x, y))
158
+ else:
159
+ p.lineToPoint_((x, y))
160
+ p.closePath()
161
+ return p
162
+
163
+ class WaveView(AppKit.NSView): # noqa: D401
164
+ """A little glowing-blue ghost whose mouth opens with your voice.
165
+
166
+ A friendly translucent ghost floats at the bottom of the screen:
167
+ it bobs and blinks on its own so it feels alive, and its mouth
168
+ opens wider the louder you speak (driven by the live,
169
+ gain-adjusted mic level) -- the Animoji-style talking face that
170
+ makes the tool fun to use.
171
+ """
172
+
173
+ def initWithFrame_(self, frame): # noqa: ANN001, ANN201, N802
174
+ self = objc.super(WaveView, self).initWithFrame_(frame)
175
+ if self is None:
176
+ return None
177
+ self.amp = 0.0 # smoothed level -> mouth opening
178
+ self.phase = 0.0
179
+ self.frame_no = 0
180
+ self.recording = False
181
+ return self
182
+
183
+ def push_(self, sample): # noqa: ANN001, ANN201, N802
184
+ _label, recording, level = sample
185
+ level = max(0.0, min(1.0, float(level)))
186
+ # Gentle attack, slow release so the mouth and motion change
187
+ # gradually rather than twitching frame-to-frame.
188
+ k = 0.4 if level > self.amp else 0.10
189
+ self.amp += k * (level - self.amp)
190
+ self.phase += _PHASE_SPEED * speed
191
+ self.frame_no += 1
192
+ self.recording = bool(recording)
193
+ self.setNeedsDisplay_(True)
194
+
195
+ def drawRect_(self, rect): # noqa: ANN001, ANN201, N802
196
+ import math
197
+
198
+ b = self.bounds()
199
+ cx, cy = b.size.width / 2, b.size.height / 2
200
+ gw = min(b.size.width, b.size.height) * 0.22
201
+ amp = self.amp
202
+ ph = self.phase
203
+
204
+ # Slow, gentle motion — a gradual drift, not a vibration.
205
+ # Bob and sway run at DIFFERENT slow frequencies so the
206
+ # ghost describes a lazy figure-eight rather than a rigid
207
+ # vertical bounce; both grow only mildly with volume.
208
+ bob = (3.0 + 3.0 * amp) * math.sin(ph * 0.9)
209
+ sway = (3.0 + 4.5 * amp) * math.sin(ph * 0.6 + 0.8)
210
+ dcx, dcy = cx + sway, cy + bob
211
+ # The whole face gently breathes and EXPANDS with volume
212
+ # (uniform scale — grows/shrinks, no shape distortion), plus
213
+ # a soft left/right lean. That is the head's audio reaction.
214
+ scale = (1.0 + 0.03 * math.sin(ph * 0.7)) * (1.0 + 0.11 * amp)
215
+ sx = sy = scale
216
+ lean = (2.5 + 3.0 * amp) * math.sin(ph * 0.45) # degrees
217
+
218
+ NSGraphicsContext.currentContext().saveGraphicsState()
219
+ t = NSAffineTransform.transform()
220
+ t.translateXBy_yBy_(dcx, dcy)
221
+ t.rotateByDegrees_(lean)
222
+ t.scaleXBy_yBy_(sx, sy)
223
+ t.translateXBy_yBy_(-dcx, -dcy)
224
+ t.concat()
225
+ try:
226
+ # Slow, spatially-coherent jelly flex so the head is
227
+ # visibly non-rigid; more when talking, and scaled by the
228
+ # user's overlay_flex.
229
+ jelly = (2.6 + 5.0 * amp) * flex
230
+
231
+ # soft halo for presence (flexes WITH the body)
232
+ halo = _mk_ghost(dcx, dcy, gw * 1.10, jelly, ph)
233
+ NSColor.colorWithCalibratedRed_green_blue_alpha_(
234
+ 0.30, 0.62, 1.0, 0.20
235
+ ).setFill()
236
+ halo.fill()
237
+
238
+ # body: bluish radial gradient + bright rim
239
+ body = _mk_ghost(dcx, dcy, gw, jelly, ph)
240
+ core = NSColor.colorWithCalibratedRed_green_blue_alpha_(
241
+ 0.62, 0.86, 1.0, 0.88
242
+ )
243
+ edge = NSColor.colorWithCalibratedRed_green_blue_alpha_(
244
+ 0.16, 0.44, 0.98, 0.74
245
+ )
246
+ grad = (
247
+ NSGradient.alloc().initWithStartingColor_endingColor_(
248
+ core, edge
249
+ )
250
+ )
251
+ if grad is not None:
252
+ grad.drawInBezierPath_relativeCenterPosition_(
253
+ body, (0.0, 0.35)
254
+ )
255
+ else: # pragma: no cover
256
+ edge.setFill()
257
+ body.fill()
258
+ NSColor.colorWithCalibratedRed_green_blue_alpha_(
259
+ 0.80, 0.94, 1.0, 0.9
260
+ ).setStroke()
261
+ body.setLineWidth_(2.0)
262
+ body.stroke()
263
+
264
+ # --- face features, CLIPPED to the body so nothing (the
265
+ # mouth in particular) can ever spill past the silhouette
266
+ gc = NSGraphicsContext.currentContext()
267
+ gc.saveGraphicsState()
268
+ body.addClip()
269
+ dark = NSColor.colorWithCalibratedRed_green_blue_alpha_(
270
+ 0.05, 0.10, 0.26, 0.92
271
+ )
272
+ dark.setFill()
273
+ eye_y = dcy + gw * 0.45
274
+ eye_dx = gw * 0.42
275
+ blinking = (
276
+ self.frame_no % _BLINK_EVERY
277
+ ) < _BLINK_FRAMES
278
+ eye_w = gw * 0.26
279
+ eye_h = gw * 0.06 if blinking else gw * 0.36
280
+ for s in (-1, 1):
281
+ _mk_oval(
282
+ dcx + s * eye_dx, eye_y, eye_w, eye_h
283
+ ).fill()
284
+ _mk_oval(dcx, dcy + gw * 0.05, gw * 0.10, gw * 0.12).fill()
285
+ lip_y = dcy - gw * 0.18
286
+ _mk_mouth(dcx, lip_y, gw, amp, ph).fill()
287
+ gc.restoreGraphicsState()
288
+ finally:
289
+ NSGraphicsContext.currentContext().restoreGraphicsState()
290
+
291
+ class Driver(AppKit.NSObject):
292
+ """NSTimer target: samples audio, ticks the app, ends the loop.
293
+
294
+ The pill is shown ONLY while recording — its mere presence is
295
+ the "you left the mic on" indicator; absence means not
296
+ listening. (A color change alone proved too subtle.)
297
+ """
298
+
299
+ def initWithView_panel_(self, view, panel): # noqa: ANN001, ANN201, N802, E501
300
+ self = objc.super(Driver, self).init()
301
+ if self is None:
302
+ return None
303
+ self.view = view
304
+ self.panel = panel
305
+ self.shown = False
306
+ return self
307
+
308
+ def tick_(self, _timer): # noqa: ANN001, ANN201, N802
309
+ try:
310
+ tick()
311
+ except Exception:
312
+ pass
313
+ try:
314
+ data = sample()
315
+ recording = bool(data[1])
316
+ if recording != self.shown:
317
+ if recording:
318
+ self.panel.orderFrontRegardless()
319
+ else:
320
+ self.panel.orderOut_(None)
321
+ self.shown = recording
322
+ if recording:
323
+ self.view.push_(data)
324
+ except Exception:
325
+ pass
326
+ if stopped():
327
+ NSApplication.sharedApplication().stop_(None)
328
+ # stop_() only takes effect once an event is processed;
329
+ # post a no-op event so the run loop wakes immediately.
330
+ evt = AppKit.NSEvent.otherEventWithType_location_modifierFlags_timestamp_windowNumber_context_subtype_data1_data2_( # noqa: E501
331
+ AppKit.NSEventTypeApplicationDefined,
332
+ (0, 0), 0, 0.0, 0, None, 0, 0, 0,
333
+ )
334
+ NSApplication.sharedApplication().postEvent_atStart_(
335
+ evt, True
336
+ )
337
+
338
+ app = NSApplication.sharedApplication()
339
+ # Accessory: no Dock icon, no menu bar, but windows still display.
340
+ app.setActivationPolicy_(
341
+ AppKit.NSApplicationActivationPolicyAccessory
342
+ )
343
+
344
+ screen = NSScreen.mainScreen()
345
+ sframe = screen.frame() if screen is not None else NSMakeRect(
346
+ 0, 0, 1440, 900
347
+ )
348
+ x = sframe.origin.x + (sframe.size.width - _WIDTH) / 2
349
+ y = sframe.origin.y + _MARGIN_BOTTOM
350
+ panel = NSPanel.alloc().initWithContentRect_styleMask_backing_defer_(
351
+ NSMakeRect(x, y, _WIDTH, _HEIGHT),
352
+ AppKit.NSWindowStyleMaskBorderless
353
+ | AppKit.NSWindowStyleMaskNonactivatingPanel,
354
+ NSBackingStoreBuffered,
355
+ False,
356
+ )
357
+ panel.setLevel_(AppKit.NSStatusWindowLevel)
358
+ panel.setOpaque_(False)
359
+ panel.setBackgroundColor_(NSColor.clearColor())
360
+ panel.setIgnoresMouseEvents_(True)
361
+ panel.setHasShadow_(True)
362
+ panel.setCollectionBehavior_(
363
+ AppKit.NSWindowCollectionBehaviorCanJoinAllSpaces
364
+ | AppKit.NSWindowCollectionBehaviorStationary
365
+ )
366
+ view = WaveView.alloc().initWithFrame_(
367
+ NSMakeRect(0, 0, _WIDTH, _HEIGHT)
368
+ )
369
+ panel.setContentView_(view)
370
+ # Hidden at startup; the driver shows it only while recording.
371
+
372
+ driver = Driver.alloc().initWithView_panel_(view, panel)
373
+ timer = NSTimer.scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_( # noqa: E501
374
+ _TICK_SECONDS, driver, b"tick:", None, True
375
+ )
376
+ AppKit.NSRunLoop.currentRunLoop().addTimer_forMode_(
377
+ timer, AppKit.NSRunLoopCommonModes
378
+ )
379
+
380
+ # Ctrl+C: AppKit's run loop doesn't deliver KeyboardInterrupt on
381
+ # its own. Record the signal in a flag the timer polls (the timer
382
+ # firing is what lets the Python-level handler run at all), end
383
+ # the loop, and re-raise KeyboardInterrupt for the caller.
384
+ sigint = {"hit": False}
385
+ caller_stopped = stopped
386
+
387
+ def stopped() -> bool: # noqa: ANN202
388
+ return sigint["hit"] or caller_stopped()
389
+
390
+ previous_handler = signal.getsignal(signal.SIGINT)
391
+ signal.signal(
392
+ signal.SIGINT,
393
+ lambda _sig, _frame: sigint.__setitem__("hit", True),
394
+ )
395
+ try:
396
+ app.run()
397
+ finally:
398
+ signal.signal(signal.SIGINT, previous_handler)
399
+ timer.invalidate()
400
+ panel.orderOut_(None)
401
+ if sigint["hit"]:
402
+ raise KeyboardInterrupt
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "claude-code-tools"
3
- version = "1.19.6"
3
+ version = "1.19.7"
4
4
  description = "Collection of tools for working with Claude Code"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -131,7 +131,7 @@ markers = [
131
131
 
132
132
  [tool.commitizen]
133
133
  name = "cz_conventional_commits"
134
- version = "1.19.6"
134
+ version = "1.19.7"
135
135
  tag_format = "v$version"
136
136
  version_files = [
137
137
  "pyproject.toml:version",
@@ -1,273 +0,0 @@
1
- """Floating waveform pill: visible proof that voice-type is listening.
2
-
3
- A small always-on-top, click-through, non-activating macOS panel at the
4
- bottom-center of the screen, shown ONLY while recording. It scrolls a
5
- live waveform of the mic level — red waves as you speak — and hides
6
- when paused or waiting for the wake word. Because the panel is a
7
- non-activating borderless NSPanel that ignores mouse events, it can
8
- never steal keyboard focus from the app being dictated into.
9
-
10
- Uses pyobjc (already a transitive dependency of pynput on macOS).
11
- The overlay must run on the process's MAIN thread (AppKit rule); the
12
- app hands its per-tick housekeeping in as a callback.
13
- """
14
-
15
- from __future__ import annotations
16
-
17
- import signal
18
- from typing import Callable
19
-
20
- # (state_label, is_recording, level 0..1) sampled ~20x per second.
21
- SampleFn = Callable[[], tuple[str, bool, float]]
22
- TickFn = Callable[[], None]
23
-
24
- _WIDTH = 200.0
25
- _HEIGHT = 150.0
26
- _MARGIN_BOTTOM = 120.0
27
- # Points around the blob's rim (smoothness) and frame rate.
28
- _BLOB_POINTS = 108
29
- _TICK_SECONDS = 0.033 # ~30 fps, smooth wobble
30
-
31
-
32
- def overlay_available() -> bool:
33
- """True if the AppKit bridge is importable (macOS with pyobjc)."""
34
- try:
35
- import AppKit # noqa: F401
36
-
37
- return True
38
- except Exception:
39
- return False
40
-
41
-
42
- def run_overlay(sample: SampleFn, tick: TickFn, stopped: Callable[[], bool]) -> None:
43
- """Show the pill and block until ``stopped()`` returns True.
44
-
45
- Args:
46
- sample: Returns (state_label, is_recording, level) each frame.
47
- tick: App housekeeping to run each frame (idle timeout, fatal
48
- checks); exceptions are swallowed so the UI never dies.
49
- stopped: Polled each frame; True ends the loop and closes the
50
- panel.
51
-
52
- SIGINT is redirected to a flag-friendly handler while the loop
53
- runs (AppKit's run loop would otherwise swallow Ctrl+C), and
54
- restored afterwards.
55
- """
56
- import AppKit
57
- import objc
58
- from AppKit import (
59
- NSApplication,
60
- NSBackingStoreBuffered,
61
- NSBezierPath,
62
- NSColor,
63
- NSGradient,
64
- NSMakeRect,
65
- NSPanel,
66
- NSScreen,
67
- NSTimer,
68
- )
69
-
70
- class WaveView(AppKit.NSView): # noqa: D401
71
- """A glowing blue blob that wobbles and swells with the voice.
72
-
73
- A soft orb sits at the bottom of the screen: it breathes gently
74
- in silence and, as the live gain-adjusted mic level rises, its
75
- rim deforms (organic multi-harmonic wobble) and the whole blob
76
- swells — so its shape reflects the actual audio. Bluish, with a
77
- radial gradient core, a rim highlight, and an outer halo for
78
- presence.
79
- """
80
-
81
- def initWithFrame_(self, frame): # noqa: ANN001, ANN201, N802
82
- self = objc.super(WaveView, self).initWithFrame_(frame)
83
- if self is None:
84
- return None
85
- self.amp = 0.0 # smoothed audio level driving the wobble
86
- self.phase = 0.0
87
- self.recording = False
88
- return self
89
-
90
- def push_(self, sample): # noqa: ANN001, ANN201, N802
91
- _label, recording, level = sample
92
- level = max(0.0, min(1.0, float(level)))
93
- # Fast attack, slow release: pops on speech, settles gently.
94
- k = 0.55 if level > self.amp else 0.15
95
- self.amp += k * (level - self.amp)
96
- self.phase += 0.16
97
- self.recording = bool(recording)
98
- self.setNeedsDisplay_(True)
99
-
100
- def _blob_path(self, cx, cy, base_r): # noqa: ANN001, ANN202
101
- import math
102
-
103
- path = NSBezierPath.bezierPath()
104
- wob = 0.09 + 0.42 * self.amp # rim deformation depth
105
- breathe = 0.05 * math.sin(self.phase * 0.5)
106
- swell = 1.0 + 0.24 * self.amp # overall growth with volume
107
- for i in range(_BLOB_POINTS + 1):
108
- a = 2.0 * math.pi * i / _BLOB_POINTS
109
- shape = (
110
- math.sin(3 * a + self.phase)
111
- + 0.6 * math.sin(5 * a - 1.3 * self.phase)
112
- + 0.3 * math.sin(7 * a + 1.7 * self.phase)
113
- ) / 1.9
114
- r = base_r * swell * (1.0 + breathe + wob * shape)
115
- x = cx + r * math.cos(a)
116
- y = cy + r * math.sin(a)
117
- if i == 0:
118
- path.moveToPoint_((x, y))
119
- else:
120
- path.lineToPoint_((x, y))
121
- path.closePath()
122
- return path
123
-
124
- def drawRect_(self, rect): # noqa: ANN001, ANN201, N802
125
- b = self.bounds()
126
- cx, cy = b.size.width / 2, b.size.height / 2
127
- base_r = min(b.size.width, b.size.height) * 0.26
128
-
129
- # outer halo for presence
130
- halo = self._blob_path(cx, cy, base_r * 1.28)
131
- NSColor.colorWithCalibratedRed_green_blue_alpha_(
132
- 0.30, 0.62, 1.0, 0.20
133
- ).setFill()
134
- halo.fill()
135
-
136
- # the blob: radial gradient core -> deeper blue rim
137
- blob = self._blob_path(cx, cy, base_r)
138
- core = NSColor.colorWithCalibratedRed_green_blue_alpha_(
139
- 0.60, 0.85, 1.0, 0.85
140
- )
141
- edge = NSColor.colorWithCalibratedRed_green_blue_alpha_(
142
- 0.16, 0.44, 0.98, 0.70
143
- )
144
- grad = NSGradient.alloc().initWithStartingColor_endingColor_(
145
- core, edge
146
- )
147
- if grad is not None:
148
- grad.drawInBezierPath_relativeCenterPosition_(
149
- blob, (0.0, 0.28)
150
- )
151
- else: # pragma: no cover - gradient unavailable
152
- edge.setFill()
153
- blob.fill()
154
-
155
- # bright rim
156
- NSColor.colorWithCalibratedRed_green_blue_alpha_(
157
- 0.75, 0.92, 1.0, 0.9
158
- ).setStroke()
159
- blob.setLineWidth_(2.0)
160
- blob.stroke()
161
-
162
- class Driver(AppKit.NSObject):
163
- """NSTimer target: samples audio, ticks the app, ends the loop.
164
-
165
- The pill is shown ONLY while recording — its mere presence is
166
- the "you left the mic on" indicator; absence means not
167
- listening. (A color change alone proved too subtle.)
168
- """
169
-
170
- def initWithView_panel_(self, view, panel): # noqa: ANN001, ANN201, N802, E501
171
- self = objc.super(Driver, self).init()
172
- if self is None:
173
- return None
174
- self.view = view
175
- self.panel = panel
176
- self.shown = False
177
- return self
178
-
179
- def tick_(self, _timer): # noqa: ANN001, ANN201, N802
180
- try:
181
- tick()
182
- except Exception:
183
- pass
184
- try:
185
- data = sample()
186
- recording = bool(data[1])
187
- if recording != self.shown:
188
- if recording:
189
- self.panel.orderFrontRegardless()
190
- else:
191
- self.panel.orderOut_(None)
192
- self.shown = recording
193
- if recording:
194
- self.view.push_(data)
195
- except Exception:
196
- pass
197
- if stopped():
198
- NSApplication.sharedApplication().stop_(None)
199
- # stop_() only takes effect once an event is processed;
200
- # post a no-op event so the run loop wakes immediately.
201
- evt = AppKit.NSEvent.otherEventWithType_location_modifierFlags_timestamp_windowNumber_context_subtype_data1_data2_( # noqa: E501
202
- AppKit.NSEventTypeApplicationDefined,
203
- (0, 0), 0, 0.0, 0, None, 0, 0, 0,
204
- )
205
- NSApplication.sharedApplication().postEvent_atStart_(
206
- evt, True
207
- )
208
-
209
- app = NSApplication.sharedApplication()
210
- # Accessory: no Dock icon, no menu bar, but windows still display.
211
- app.setActivationPolicy_(
212
- AppKit.NSApplicationActivationPolicyAccessory
213
- )
214
-
215
- screen = NSScreen.mainScreen()
216
- sframe = screen.frame() if screen is not None else NSMakeRect(
217
- 0, 0, 1440, 900
218
- )
219
- x = sframe.origin.x + (sframe.size.width - _WIDTH) / 2
220
- y = sframe.origin.y + _MARGIN_BOTTOM
221
- panel = NSPanel.alloc().initWithContentRect_styleMask_backing_defer_(
222
- NSMakeRect(x, y, _WIDTH, _HEIGHT),
223
- AppKit.NSWindowStyleMaskBorderless
224
- | AppKit.NSWindowStyleMaskNonactivatingPanel,
225
- NSBackingStoreBuffered,
226
- False,
227
- )
228
- panel.setLevel_(AppKit.NSStatusWindowLevel)
229
- panel.setOpaque_(False)
230
- panel.setBackgroundColor_(NSColor.clearColor())
231
- panel.setIgnoresMouseEvents_(True)
232
- panel.setHasShadow_(True)
233
- panel.setCollectionBehavior_(
234
- AppKit.NSWindowCollectionBehaviorCanJoinAllSpaces
235
- | AppKit.NSWindowCollectionBehaviorStationary
236
- )
237
- view = WaveView.alloc().initWithFrame_(
238
- NSMakeRect(0, 0, _WIDTH, _HEIGHT)
239
- )
240
- panel.setContentView_(view)
241
- # Hidden at startup; the driver shows it only while recording.
242
-
243
- driver = Driver.alloc().initWithView_panel_(view, panel)
244
- timer = NSTimer.scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_( # noqa: E501
245
- _TICK_SECONDS, driver, b"tick:", None, True
246
- )
247
- AppKit.NSRunLoop.currentRunLoop().addTimer_forMode_(
248
- timer, AppKit.NSRunLoopCommonModes
249
- )
250
-
251
- # Ctrl+C: AppKit's run loop doesn't deliver KeyboardInterrupt on
252
- # its own. Record the signal in a flag the timer polls (the timer
253
- # firing is what lets the Python-level handler run at all), end
254
- # the loop, and re-raise KeyboardInterrupt for the caller.
255
- sigint = {"hit": False}
256
- caller_stopped = stopped
257
-
258
- def stopped() -> bool: # noqa: ANN202
259
- return sigint["hit"] or caller_stopped()
260
-
261
- previous_handler = signal.getsignal(signal.SIGINT)
262
- signal.signal(
263
- signal.SIGINT,
264
- lambda _sig, _frame: sigint.__setitem__("hit", True),
265
- )
266
- try:
267
- app.run()
268
- finally:
269
- signal.signal(signal.SIGINT, previous_handler)
270
- timer.invalidate()
271
- panel.orderOut_(None)
272
- if sigint["hit"]:
273
- raise KeyboardInterrupt