claude-code-tools 1.19.5__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.5 → claude_code_tools-1.19.7}/PKG-INFO +1 -1
  2. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/__init__.py +1 -1
  3. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/voice_type/app.py +5 -1
  4. {claude_code_tools-1.19.5 → 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.5 → claude_code_tools-1.19.7}/pyproject.toml +2 -2
  7. claude_code_tools-1.19.5/claude_code_tools/voice_type/overlay.py +0 -272
  8. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/.gitignore +0 -0
  9. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/LICENSE +0 -0
  10. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/README.md +0 -0
  11. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/action_rpc.py +0 -0
  12. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/agent_tunnel/__init__.py +0 -0
  13. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/agent_tunnel/backends.py +0 -0
  14. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/agent_tunnel/cli.py +0 -0
  15. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/agent_tunnel/config.py +0 -0
  16. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/agent_tunnel/convert.py +0 -0
  17. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/agent_tunnel/discord_bot.py +0 -0
  18. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/agent_tunnel/locking.py +0 -0
  19. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/agent_tunnel/paths.py +0 -0
  20. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/agent_tunnel/registry.py +0 -0
  21. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/agent_tunnel/session.py +0 -0
  22. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/agent_tunnel/store.py +0 -0
  23. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/agent_tunnel/tmux.py +0 -0
  24. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/agent_tunnel/trust.py +0 -0
  25. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/aichat.py +0 -0
  26. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/claude_continue.py +0 -0
  27. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/codex_continue.py +0 -0
  28. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/codex_server.py +0 -0
  29. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/codex_server_cli.py +0 -0
  30. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/codex_server_fingerprint.py +0 -0
  31. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/codex_server_generation.py +0 -0
  32. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/codex_server_legacy.py +0 -0
  33. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/codex_server_models.py +0 -0
  34. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/codex_server_process.py +0 -0
  35. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/codex_server_reservation.py +0 -0
  36. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/codex_server_retry.py +0 -0
  37. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/codex_server_reuse.py +0 -0
  38. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/codex_server_state.py +0 -0
  39. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/codex_server_supervisor.py +0 -0
  40. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/codex_server_worker.py +0 -0
  41. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/config.py +0 -0
  42. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/csv2gsheet.py +0 -0
  43. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/delete_session.py +0 -0
  44. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/dotenv_vault.py +0 -0
  45. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/env_safe.py +0 -0
  46. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/export_all.py +0 -0
  47. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/export_claude_session.py +0 -0
  48. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/export_codex_session.py +0 -0
  49. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/export_session.py +0 -0
  50. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/find_claude_session.py +0 -0
  51. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/find_codex_session.py +0 -0
  52. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/find_original_session.py +0 -0
  53. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/find_session.py +0 -0
  54. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/find_trimmed_sessions.py +0 -0
  55. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/fix_session.py +0 -0
  56. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/gdoc2docx.py +0 -0
  57. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/gdoc2md.py +0 -0
  58. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/gsheet2csv.py +0 -0
  59. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/md2gdoc.py +0 -0
  60. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/msg/__init__.py +0 -0
  61. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/msg/cli.py +0 -0
  62. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/msg/hooks.py +0 -0
  63. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/msg/models.py +0 -0
  64. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/msg/prompt_detect.py +0 -0
  65. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/msg/store.py +0 -0
  66. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/msg/watcher.py +0 -0
  67. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/node_menu_ui.py +0 -0
  68. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/port_claude_noise.py +0 -0
  69. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/port_claude_to_codex.py +0 -0
  70. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/port_codex_flatten.py +0 -0
  71. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/port_codex_to_claude.py +0 -0
  72. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/port_service.py +0 -0
  73. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/resolve_session.py +0 -0
  74. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/resolve_session_names.py +0 -0
  75. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/resolve_session_render.py +0 -0
  76. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/search_index.py +0 -0
  77. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/session_lineage.py +0 -0
  78. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/session_menu.py +0 -0
  79. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/session_menu_cli.py +0 -0
  80. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/session_utils.py +0 -0
  81. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/smart_trim.py +0 -0
  82. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/smart_trim_core.py +0 -0
  83. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/tmux_cli_controller.py +0 -0
  84. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/tmux_execution_helpers.py +0 -0
  85. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/tmux_remote_controller.py +0 -0
  86. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/trim_in_place.py +0 -0
  87. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/trim_session.py +0 -0
  88. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/trim_session_claude.py +0 -0
  89. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/trim_session_codex.py +0 -0
  90. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/voice_type/__init__.py +0 -0
  91. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/voice_type/cli.py +0 -0
  92. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/voice_type/engine_parakeet.py +0 -0
  93. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/voice_type/engine_parakeet_mlx.py +0 -0
  94. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/voice_type/engines.py +0 -0
  95. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/voice_type/hotkey.py +0 -0
  96. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/voice_type/inject.py +0 -0
  97. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/voice_type/logic.py +0 -0
  98. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/workflow_cli.py +0 -0
  99. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/workflow_cli_contract.py +0 -0
  100. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/workflow_cli_formatting.py +0 -0
  101. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/workflow_cli_identity_policy.py +0 -0
  102. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/workflow_cli_manifest.py +0 -0
  103. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/workflow_cli_projection.py +0 -0
  104. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/workflow_cli_rendering.py +0 -0
  105. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/workflow_cli_snapshots.py +0 -0
  106. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/workflow_cli_store_backends.py +0 -0
  107. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/workflow_processes.py +0 -0
  108. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/workflow_runs.py +0 -0
  109. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/workflow_store_io.py +0 -0
  110. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/claude_code_tools/workflow_validation.py +0 -0
  111. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/docs/agent-tunnel-spec.md +0 -0
  112. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/docs/claude-code-tmux-tutorials.md +0 -0
  113. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/docs/dot-zshrc.md +0 -0
  114. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/docs/find-claude-session.md +0 -0
  115. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/docs/intercom-spec.md +0 -0
  116. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/docs/lmsh.md +0 -0
  117. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/docs/local-llm-setup.md +0 -0
  118. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/docs/msg-phase-a-plan.md +0 -0
  119. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/docs/msg-plan-v2.md +0 -0
  120. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/docs/tmux-cli-instructions.md +0 -0
  121. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/docs/vault-documentation.md +0 -0
  122. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/hatch_build.py +0 -0
  123. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/node_ui/action_config.js +0 -0
  124. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/node_ui/menu.js +0 -0
  125. {claude_code_tools-1.19.5 → claude_code_tools-1.19.7}/node_ui/package-lock.json +0 -0
  126. {claude_code_tools-1.19.5 → 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.5
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.5"
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.5"
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.5"
134
+ version = "1.19.7"
135
135
  tag_format = "v$version"
136
136
  version_files = [
137
137
  "pyproject.toml:version",
@@ -1,272 +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 = 460.0
25
- _HEIGHT = 92.0
26
- _MARGIN_BOTTOM = 110.0
27
- # Recent audio-level history (drives the local vibration envelope) and
28
- # the number of points the string is rendered with (smoothness).
29
- _HISTORY = 96
30
- _RENDER = 190
31
- _TICK_SECONDS = 0.033 # ~30 fps, smooth string motion
32
-
33
-
34
- def overlay_available() -> bool:
35
- """True if the AppKit bridge is importable (macOS with pyobjc)."""
36
- try:
37
- import AppKit # noqa: F401
38
-
39
- return True
40
- except Exception:
41
- return False
42
-
43
-
44
- def run_overlay(sample: SampleFn, tick: TickFn, stopped: Callable[[], bool]) -> None:
45
- """Show the pill and block until ``stopped()`` returns True.
46
-
47
- Args:
48
- sample: Returns (state_label, is_recording, level) each frame.
49
- tick: App housekeeping to run each frame (idle timeout, fatal
50
- checks); exceptions are swallowed so the UI never dies.
51
- stopped: Polled each frame; True ends the loop and closes the
52
- panel.
53
-
54
- SIGINT is redirected to a flag-friendly handler while the loop
55
- runs (AppKit's run loop would otherwise swallow Ctrl+C), and
56
- restored afterwards.
57
- """
58
- import AppKit
59
- import objc
60
- from AppKit import (
61
- NSApplication,
62
- NSBackingStoreBuffered,
63
- NSBezierPath,
64
- NSColor,
65
- NSMakeRect,
66
- NSPanel,
67
- NSScreen,
68
- NSTimer,
69
- )
70
-
71
- class WaveView(AppKit.NSView): # noqa: D401
72
- """A translucent string that lies flat and vibrates with the voice.
73
-
74
- The line is pinned at both ends (like a plucked string) and its
75
- oscillation amplitude tracks the live, gain-adjusted mic level:
76
- silence reads as a near-flat line, speech makes it ripple. The
77
- recent-level history modulates the amplitude ALONG the string
78
- (and scrolls), so the shape reflects the actual audio rather
79
- than a fixed animation.
80
- """
81
-
82
- def initWithFrame_(self, frame): # noqa: ANN001, ANN201, N802
83
- self = objc.super(WaveView, self).initWithFrame_(frame)
84
- if self is None:
85
- return None
86
- self.levels = [0.0] * _HISTORY
87
- self.phase = 0.0
88
- self.recording = False
89
- return self
90
-
91
- def push_(self, sample): # noqa: ANN001, ANN201, N802
92
- _label, recording, level = sample
93
- level = max(0.0, min(1.0, float(level)))
94
- # Mild smoothing so the string settles rather than jitters.
95
- prev = self.levels[-1]
96
- smoothed = prev + 0.5 * (level - prev)
97
- self.levels = self.levels[1:] + [smoothed]
98
- self.phase += 0.42 # advance the travelling wave each frame
99
- self.recording = bool(recording)
100
- self.setNeedsDisplay_(True)
101
-
102
- def _string_path(self, width, mid, max_amp): # noqa: ANN001, ANN202
103
- import math
104
-
105
- path = NSBezierPath.bezierPath()
106
- path.setLineJoinStyle_(AppKit.NSLineJoinStyleRound)
107
- path.setLineCapStyle_(AppKit.NSLineCapStyleRound)
108
- n = len(self.levels)
109
- for i in range(_RENDER + 1):
110
- u = i / _RENDER # 0..1 along the string
111
- # local amplitude from the level history at this point
112
- fpos = u * (n - 1)
113
- lo = int(fpos)
114
- frac = fpos - lo
115
- lvl = self.levels[lo]
116
- if lo + 1 < n:
117
- lvl += frac * (self.levels[lo + 1] - lvl)
118
- window = math.sin(math.pi * u) # pin both ends to flat
119
- amp = lvl * max_amp * window
120
- disp = amp * (
121
- math.sin(u * 7.0 * math.pi + self.phase)
122
- + 0.4 * math.sin(u * 13.0 * math.pi - 1.7 * self.phase)
123
- ) / 1.4
124
- x = u * width
125
- y = mid + disp
126
- if i == 0:
127
- path.moveToPoint_((x, y))
128
- else:
129
- path.lineToPoint_((x, y))
130
- return path
131
-
132
- def drawRect_(self, rect): # noqa: ANN001, ANN201, N802
133
- b = self.bounds()
134
- # Ultra-faint rounded backdrop: just enough to read the
135
- # string over any wallpaper, without looking like a panel.
136
- pad = 6.0
137
- back = NSMakeRect(
138
- pad, pad, b.size.width - 2 * pad, b.size.height - 2 * pad
139
- )
140
- r = back.size.height / 2
141
- NSColor.colorWithCalibratedWhite_alpha_(0.0, 0.16).setFill()
142
- NSBezierPath.bezierPathWithRoundedRect_xRadius_yRadius_(
143
- back, r, r
144
- ).fill()
145
-
146
- mid = b.size.height / 2
147
- max_amp = b.size.height / 2 - 16.0
148
- path = self._string_path(b.size.width, mid, max_amp)
149
- # soft glow behind, then the crisp translucent string
150
- NSColor.colorWithCalibratedRed_green_blue_alpha_(
151
- 1.0, 0.30, 0.26, 0.16
152
- ).setStroke()
153
- path.setLineWidth_(7.0)
154
- path.stroke()
155
- NSColor.colorWithCalibratedRed_green_blue_alpha_(
156
- 1.0, 0.42, 0.38, 0.55
157
- ).setStroke()
158
- path.setLineWidth_(2.0)
159
- path.stroke()
160
-
161
- class Driver(AppKit.NSObject):
162
- """NSTimer target: samples audio, ticks the app, ends the loop.
163
-
164
- The pill is shown ONLY while recording — its mere presence is
165
- the "you left the mic on" indicator; absence means not
166
- listening. (A color change alone proved too subtle.)
167
- """
168
-
169
- def initWithView_panel_(self, view, panel): # noqa: ANN001, ANN201, N802, E501
170
- self = objc.super(Driver, self).init()
171
- if self is None:
172
- return None
173
- self.view = view
174
- self.panel = panel
175
- self.shown = False
176
- return self
177
-
178
- def tick_(self, _timer): # noqa: ANN001, ANN201, N802
179
- try:
180
- tick()
181
- except Exception:
182
- pass
183
- try:
184
- data = sample()
185
- recording = bool(data[1])
186
- if recording != self.shown:
187
- if recording:
188
- self.panel.orderFrontRegardless()
189
- else:
190
- self.panel.orderOut_(None)
191
- self.shown = recording
192
- if recording:
193
- self.view.push_(data)
194
- except Exception:
195
- pass
196
- if stopped():
197
- NSApplication.sharedApplication().stop_(None)
198
- # stop_() only takes effect once an event is processed;
199
- # post a no-op event so the run loop wakes immediately.
200
- evt = AppKit.NSEvent.otherEventWithType_location_modifierFlags_timestamp_windowNumber_context_subtype_data1_data2_( # noqa: E501
201
- AppKit.NSEventTypeApplicationDefined,
202
- (0, 0), 0, 0.0, 0, None, 0, 0, 0,
203
- )
204
- NSApplication.sharedApplication().postEvent_atStart_(
205
- evt, True
206
- )
207
-
208
- app = NSApplication.sharedApplication()
209
- # Accessory: no Dock icon, no menu bar, but windows still display.
210
- app.setActivationPolicy_(
211
- AppKit.NSApplicationActivationPolicyAccessory
212
- )
213
-
214
- screen = NSScreen.mainScreen()
215
- sframe = screen.frame() if screen is not None else NSMakeRect(
216
- 0, 0, 1440, 900
217
- )
218
- x = sframe.origin.x + (sframe.size.width - _WIDTH) / 2
219
- y = sframe.origin.y + _MARGIN_BOTTOM
220
- panel = NSPanel.alloc().initWithContentRect_styleMask_backing_defer_(
221
- NSMakeRect(x, y, _WIDTH, _HEIGHT),
222
- AppKit.NSWindowStyleMaskBorderless
223
- | AppKit.NSWindowStyleMaskNonactivatingPanel,
224
- NSBackingStoreBuffered,
225
- False,
226
- )
227
- panel.setLevel_(AppKit.NSStatusWindowLevel)
228
- panel.setOpaque_(False)
229
- panel.setBackgroundColor_(NSColor.clearColor())
230
- panel.setIgnoresMouseEvents_(True)
231
- panel.setHasShadow_(True)
232
- panel.setCollectionBehavior_(
233
- AppKit.NSWindowCollectionBehaviorCanJoinAllSpaces
234
- | AppKit.NSWindowCollectionBehaviorStationary
235
- )
236
- view = WaveView.alloc().initWithFrame_(
237
- NSMakeRect(0, 0, _WIDTH, _HEIGHT)
238
- )
239
- panel.setContentView_(view)
240
- # Hidden at startup; the driver shows it only while recording.
241
-
242
- driver = Driver.alloc().initWithView_panel_(view, panel)
243
- timer = NSTimer.scheduledTimerWithTimeInterval_target_selector_userInfo_repeats_( # noqa: E501
244
- _TICK_SECONDS, driver, b"tick:", None, True
245
- )
246
- AppKit.NSRunLoop.currentRunLoop().addTimer_forMode_(
247
- timer, AppKit.NSRunLoopCommonModes
248
- )
249
-
250
- # Ctrl+C: AppKit's run loop doesn't deliver KeyboardInterrupt on
251
- # its own. Record the signal in a flag the timer polls (the timer
252
- # firing is what lets the Python-level handler run at all), end
253
- # the loop, and re-raise KeyboardInterrupt for the caller.
254
- sigint = {"hit": False}
255
- caller_stopped = stopped
256
-
257
- def stopped() -> bool: # noqa: ANN202
258
- return sigint["hit"] or caller_stopped()
259
-
260
- previous_handler = signal.getsignal(signal.SIGINT)
261
- signal.signal(
262
- signal.SIGINT,
263
- lambda _sig, _frame: sigint.__setitem__("hit", True),
264
- )
265
- try:
266
- app.run()
267
- finally:
268
- signal.signal(signal.SIGINT, previous_handler)
269
- timer.invalidate()
270
- panel.orderOut_(None)
271
- if sigint["hit"]:
272
- raise KeyboardInterrupt