aider-ce 0.87.2.dev9__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.
Potentially problematic release.
This version of aider-ce might be problematic. Click here for more details.
- aider/__init__.py +20 -0
- aider/__main__.py +4 -0
- aider/_version.py +34 -0
- aider/analytics.py +258 -0
- aider/args.py +1014 -0
- aider/args_formatter.py +228 -0
- aider/change_tracker.py +133 -0
- aider/coders/__init__.py +36 -0
- aider/coders/architect_coder.py +48 -0
- aider/coders/architect_prompts.py +40 -0
- aider/coders/ask_coder.py +9 -0
- aider/coders/ask_prompts.py +35 -0
- aider/coders/base_coder.py +3013 -0
- aider/coders/base_prompts.py +87 -0
- aider/coders/chat_chunks.py +64 -0
- aider/coders/context_coder.py +53 -0
- aider/coders/context_prompts.py +75 -0
- aider/coders/editblock_coder.py +657 -0
- aider/coders/editblock_fenced_coder.py +10 -0
- aider/coders/editblock_fenced_prompts.py +143 -0
- aider/coders/editblock_func_coder.py +141 -0
- aider/coders/editblock_func_prompts.py +27 -0
- aider/coders/editblock_prompts.py +177 -0
- aider/coders/editor_diff_fenced_coder.py +9 -0
- aider/coders/editor_diff_fenced_prompts.py +11 -0
- aider/coders/editor_editblock_coder.py +9 -0
- aider/coders/editor_editblock_prompts.py +21 -0
- aider/coders/editor_whole_coder.py +9 -0
- aider/coders/editor_whole_prompts.py +12 -0
- aider/coders/help_coder.py +16 -0
- aider/coders/help_prompts.py +46 -0
- aider/coders/navigator_coder.py +2711 -0
- aider/coders/navigator_legacy_prompts.py +338 -0
- aider/coders/navigator_prompts.py +530 -0
- aider/coders/patch_coder.py +706 -0
- aider/coders/patch_prompts.py +161 -0
- aider/coders/search_replace.py +757 -0
- aider/coders/shell.py +37 -0
- aider/coders/single_wholefile_func_coder.py +102 -0
- aider/coders/single_wholefile_func_prompts.py +27 -0
- aider/coders/udiff_coder.py +429 -0
- aider/coders/udiff_prompts.py +117 -0
- aider/coders/udiff_simple.py +14 -0
- aider/coders/udiff_simple_prompts.py +25 -0
- aider/coders/wholefile_coder.py +144 -0
- aider/coders/wholefile_func_coder.py +134 -0
- aider/coders/wholefile_func_prompts.py +27 -0
- aider/coders/wholefile_prompts.py +70 -0
- aider/commands.py +1946 -0
- aider/copypaste.py +72 -0
- aider/deprecated.py +126 -0
- aider/diffs.py +128 -0
- aider/dump.py +29 -0
- aider/editor.py +147 -0
- aider/exceptions.py +107 -0
- aider/format_settings.py +26 -0
- aider/gui.py +545 -0
- aider/help.py +163 -0
- aider/help_pats.py +19 -0
- aider/history.py +178 -0
- aider/io.py +1257 -0
- aider/linter.py +304 -0
- aider/llm.py +47 -0
- aider/main.py +1297 -0
- aider/mcp/__init__.py +94 -0
- aider/mcp/server.py +119 -0
- aider/mdstream.py +243 -0
- aider/models.py +1344 -0
- aider/onboarding.py +428 -0
- aider/openrouter.py +129 -0
- aider/prompts.py +56 -0
- aider/queries/tree-sitter-language-pack/README.md +7 -0
- aider/queries/tree-sitter-language-pack/arduino-tags.scm +5 -0
- aider/queries/tree-sitter-language-pack/c-tags.scm +9 -0
- aider/queries/tree-sitter-language-pack/chatito-tags.scm +16 -0
- aider/queries/tree-sitter-language-pack/clojure-tags.scm +7 -0
- aider/queries/tree-sitter-language-pack/commonlisp-tags.scm +122 -0
- aider/queries/tree-sitter-language-pack/cpp-tags.scm +15 -0
- aider/queries/tree-sitter-language-pack/csharp-tags.scm +26 -0
- aider/queries/tree-sitter-language-pack/d-tags.scm +26 -0
- aider/queries/tree-sitter-language-pack/dart-tags.scm +92 -0
- aider/queries/tree-sitter-language-pack/elisp-tags.scm +5 -0
- aider/queries/tree-sitter-language-pack/elixir-tags.scm +54 -0
- aider/queries/tree-sitter-language-pack/elm-tags.scm +19 -0
- aider/queries/tree-sitter-language-pack/gleam-tags.scm +41 -0
- aider/queries/tree-sitter-language-pack/go-tags.scm +42 -0
- aider/queries/tree-sitter-language-pack/java-tags.scm +20 -0
- aider/queries/tree-sitter-language-pack/javascript-tags.scm +88 -0
- aider/queries/tree-sitter-language-pack/lua-tags.scm +34 -0
- aider/queries/tree-sitter-language-pack/matlab-tags.scm +10 -0
- aider/queries/tree-sitter-language-pack/ocaml-tags.scm +115 -0
- aider/queries/tree-sitter-language-pack/ocaml_interface-tags.scm +98 -0
- aider/queries/tree-sitter-language-pack/pony-tags.scm +39 -0
- aider/queries/tree-sitter-language-pack/properties-tags.scm +5 -0
- aider/queries/tree-sitter-language-pack/python-tags.scm +14 -0
- aider/queries/tree-sitter-language-pack/r-tags.scm +21 -0
- aider/queries/tree-sitter-language-pack/racket-tags.scm +12 -0
- aider/queries/tree-sitter-language-pack/ruby-tags.scm +64 -0
- aider/queries/tree-sitter-language-pack/rust-tags.scm +60 -0
- aider/queries/tree-sitter-language-pack/solidity-tags.scm +43 -0
- aider/queries/tree-sitter-language-pack/swift-tags.scm +51 -0
- aider/queries/tree-sitter-language-pack/udev-tags.scm +20 -0
- aider/queries/tree-sitter-languages/README.md +23 -0
- aider/queries/tree-sitter-languages/c-tags.scm +9 -0
- aider/queries/tree-sitter-languages/c_sharp-tags.scm +46 -0
- aider/queries/tree-sitter-languages/cpp-tags.scm +15 -0
- aider/queries/tree-sitter-languages/dart-tags.scm +91 -0
- aider/queries/tree-sitter-languages/elisp-tags.scm +8 -0
- aider/queries/tree-sitter-languages/elixir-tags.scm +54 -0
- aider/queries/tree-sitter-languages/elm-tags.scm +19 -0
- aider/queries/tree-sitter-languages/go-tags.scm +30 -0
- aider/queries/tree-sitter-languages/hcl-tags.scm +77 -0
- aider/queries/tree-sitter-languages/java-tags.scm +20 -0
- aider/queries/tree-sitter-languages/javascript-tags.scm +88 -0
- aider/queries/tree-sitter-languages/kotlin-tags.scm +27 -0
- aider/queries/tree-sitter-languages/matlab-tags.scm +10 -0
- aider/queries/tree-sitter-languages/ocaml-tags.scm +115 -0
- aider/queries/tree-sitter-languages/ocaml_interface-tags.scm +98 -0
- aider/queries/tree-sitter-languages/php-tags.scm +26 -0
- aider/queries/tree-sitter-languages/python-tags.scm +12 -0
- aider/queries/tree-sitter-languages/ql-tags.scm +26 -0
- aider/queries/tree-sitter-languages/ruby-tags.scm +64 -0
- aider/queries/tree-sitter-languages/rust-tags.scm +60 -0
- aider/queries/tree-sitter-languages/scala-tags.scm +65 -0
- aider/queries/tree-sitter-languages/typescript-tags.scm +41 -0
- aider/reasoning_tags.py +82 -0
- aider/repo.py +621 -0
- aider/repomap.py +988 -0
- aider/report.py +200 -0
- aider/resources/__init__.py +3 -0
- aider/resources/model-metadata.json +699 -0
- aider/resources/model-settings.yml +2046 -0
- aider/run_cmd.py +132 -0
- aider/scrape.py +284 -0
- aider/sendchat.py +61 -0
- aider/special.py +203 -0
- aider/tools/__init__.py +26 -0
- aider/tools/command.py +58 -0
- aider/tools/command_interactive.py +53 -0
- aider/tools/delete_block.py +120 -0
- aider/tools/delete_line.py +112 -0
- aider/tools/delete_lines.py +137 -0
- aider/tools/extract_lines.py +276 -0
- aider/tools/grep.py +171 -0
- aider/tools/indent_lines.py +155 -0
- aider/tools/insert_block.py +211 -0
- aider/tools/list_changes.py +51 -0
- aider/tools/ls.py +49 -0
- aider/tools/make_editable.py +46 -0
- aider/tools/make_readonly.py +29 -0
- aider/tools/remove.py +48 -0
- aider/tools/replace_all.py +77 -0
- aider/tools/replace_line.py +125 -0
- aider/tools/replace_lines.py +160 -0
- aider/tools/replace_text.py +125 -0
- aider/tools/show_numbered_context.py +101 -0
- aider/tools/tool_utils.py +313 -0
- aider/tools/undo_change.py +60 -0
- aider/tools/view.py +13 -0
- aider/tools/view_files_at_glob.py +65 -0
- aider/tools/view_files_matching.py +103 -0
- aider/tools/view_files_with_symbol.py +121 -0
- aider/urls.py +17 -0
- aider/utils.py +454 -0
- aider/versioncheck.py +113 -0
- aider/voice.py +187 -0
- aider/waiting.py +221 -0
- aider/watch.py +318 -0
- aider/watch_prompts.py +12 -0
- aider/website/Gemfile +8 -0
- aider/website/_includes/blame.md +162 -0
- aider/website/_includes/get-started.md +22 -0
- aider/website/_includes/help-tip.md +5 -0
- aider/website/_includes/help.md +24 -0
- aider/website/_includes/install.md +5 -0
- aider/website/_includes/keys.md +4 -0
- aider/website/_includes/model-warnings.md +67 -0
- aider/website/_includes/multi-line.md +22 -0
- aider/website/_includes/python-m-aider.md +5 -0
- aider/website/_includes/recording.css +228 -0
- aider/website/_includes/recording.md +34 -0
- aider/website/_includes/replit-pipx.md +9 -0
- aider/website/_includes/works-best.md +1 -0
- aider/website/_sass/custom/custom.scss +103 -0
- aider/website/docs/config/adv-model-settings.md +2260 -0
- aider/website/docs/config/aider_conf.md +548 -0
- aider/website/docs/config/api-keys.md +90 -0
- aider/website/docs/config/dotenv.md +493 -0
- aider/website/docs/config/editor.md +127 -0
- aider/website/docs/config/mcp.md +95 -0
- aider/website/docs/config/model-aliases.md +104 -0
- aider/website/docs/config/options.md +890 -0
- aider/website/docs/config/reasoning.md +210 -0
- aider/website/docs/config.md +44 -0
- aider/website/docs/faq.md +384 -0
- aider/website/docs/git.md +76 -0
- aider/website/docs/index.md +47 -0
- aider/website/docs/install/codespaces.md +39 -0
- aider/website/docs/install/docker.md +57 -0
- aider/website/docs/install/optional.md +100 -0
- aider/website/docs/install/replit.md +8 -0
- aider/website/docs/install.md +115 -0
- aider/website/docs/languages.md +264 -0
- aider/website/docs/legal/contributor-agreement.md +111 -0
- aider/website/docs/legal/privacy.md +104 -0
- aider/website/docs/llms/anthropic.md +77 -0
- aider/website/docs/llms/azure.md +48 -0
- aider/website/docs/llms/bedrock.md +132 -0
- aider/website/docs/llms/cohere.md +34 -0
- aider/website/docs/llms/deepseek.md +32 -0
- aider/website/docs/llms/gemini.md +49 -0
- aider/website/docs/llms/github.md +111 -0
- aider/website/docs/llms/groq.md +36 -0
- aider/website/docs/llms/lm-studio.md +39 -0
- aider/website/docs/llms/ollama.md +75 -0
- aider/website/docs/llms/openai-compat.md +39 -0
- aider/website/docs/llms/openai.md +58 -0
- aider/website/docs/llms/openrouter.md +78 -0
- aider/website/docs/llms/other.md +111 -0
- aider/website/docs/llms/vertex.md +50 -0
- aider/website/docs/llms/warnings.md +10 -0
- aider/website/docs/llms/xai.md +53 -0
- aider/website/docs/llms.md +54 -0
- aider/website/docs/more/analytics.md +127 -0
- aider/website/docs/more/edit-formats.md +116 -0
- aider/website/docs/more/infinite-output.md +159 -0
- aider/website/docs/more-info.md +8 -0
- aider/website/docs/recordings/auto-accept-architect.md +31 -0
- aider/website/docs/recordings/dont-drop-original-read-files.md +35 -0
- aider/website/docs/recordings/index.md +21 -0
- aider/website/docs/recordings/model-accepts-settings.md +69 -0
- aider/website/docs/recordings/tree-sitter-language-pack.md +80 -0
- aider/website/docs/repomap.md +112 -0
- aider/website/docs/scripting.md +100 -0
- aider/website/docs/troubleshooting/aider-not-found.md +24 -0
- aider/website/docs/troubleshooting/edit-errors.md +76 -0
- aider/website/docs/troubleshooting/imports.md +62 -0
- aider/website/docs/troubleshooting/models-and-keys.md +54 -0
- aider/website/docs/troubleshooting/support.md +79 -0
- aider/website/docs/troubleshooting/token-limits.md +96 -0
- aider/website/docs/troubleshooting/warnings.md +12 -0
- aider/website/docs/troubleshooting.md +11 -0
- aider/website/docs/usage/browser.md +57 -0
- aider/website/docs/usage/caching.md +49 -0
- aider/website/docs/usage/commands.md +133 -0
- aider/website/docs/usage/conventions.md +119 -0
- aider/website/docs/usage/copypaste.md +121 -0
- aider/website/docs/usage/images-urls.md +48 -0
- aider/website/docs/usage/lint-test.md +118 -0
- aider/website/docs/usage/modes.md +211 -0
- aider/website/docs/usage/not-code.md +179 -0
- aider/website/docs/usage/notifications.md +87 -0
- aider/website/docs/usage/tips.md +79 -0
- aider/website/docs/usage/tutorials.md +30 -0
- aider/website/docs/usage/voice.md +121 -0
- aider/website/docs/usage/watch.md +294 -0
- aider/website/docs/usage.md +102 -0
- aider/website/share/index.md +101 -0
- aider_ce-0.87.2.dev9.dist-info/METADATA +543 -0
- aider_ce-0.87.2.dev9.dist-info/RECORD +264 -0
- aider_ce-0.87.2.dev9.dist-info/WHEEL +5 -0
- aider_ce-0.87.2.dev9.dist-info/entry_points.txt +3 -0
- aider_ce-0.87.2.dev9.dist-info/licenses/LICENSE.txt +202 -0
- aider_ce-0.87.2.dev9.dist-info/top_level.txt +1 -0
aider/voice.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import math
|
|
2
|
+
import os
|
|
3
|
+
import queue
|
|
4
|
+
import tempfile
|
|
5
|
+
import time
|
|
6
|
+
import warnings
|
|
7
|
+
|
|
8
|
+
from prompt_toolkit.shortcuts import prompt
|
|
9
|
+
|
|
10
|
+
from aider.llm import litellm
|
|
11
|
+
|
|
12
|
+
from .dump import dump # noqa: F401
|
|
13
|
+
|
|
14
|
+
warnings.filterwarnings(
|
|
15
|
+
"ignore", message="Couldn't find ffmpeg or avconv - defaulting to ffmpeg, but may not work"
|
|
16
|
+
)
|
|
17
|
+
warnings.filterwarnings("ignore", category=SyntaxWarning)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
from pydub import AudioSegment # noqa
|
|
21
|
+
from pydub.exceptions import CouldntDecodeError, CouldntEncodeError # noqa
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
import soundfile as sf
|
|
25
|
+
except (OSError, ModuleNotFoundError):
|
|
26
|
+
sf = None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SoundDeviceError(Exception):
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Voice:
|
|
34
|
+
max_rms = 0
|
|
35
|
+
min_rms = 1e5
|
|
36
|
+
pct = 0
|
|
37
|
+
|
|
38
|
+
threshold = 0.15
|
|
39
|
+
|
|
40
|
+
def __init__(self, audio_format="wav", device_name=None):
|
|
41
|
+
if sf is None:
|
|
42
|
+
raise SoundDeviceError
|
|
43
|
+
try:
|
|
44
|
+
print("Initializing sound device...")
|
|
45
|
+
import sounddevice as sd
|
|
46
|
+
|
|
47
|
+
self.sd = sd
|
|
48
|
+
|
|
49
|
+
devices = sd.query_devices()
|
|
50
|
+
|
|
51
|
+
if device_name:
|
|
52
|
+
# Find the device with matching name
|
|
53
|
+
device_id = None
|
|
54
|
+
for i, device in enumerate(devices):
|
|
55
|
+
if device_name in device["name"]:
|
|
56
|
+
device_id = i
|
|
57
|
+
break
|
|
58
|
+
if device_id is None:
|
|
59
|
+
available_inputs = [d["name"] for d in devices if d["max_input_channels"] > 0]
|
|
60
|
+
raise ValueError(
|
|
61
|
+
f"Device '{device_name}' not found. Available input devices:"
|
|
62
|
+
f" {available_inputs}"
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
print(f"Using input device: {device_name} (ID: {device_id})")
|
|
66
|
+
|
|
67
|
+
self.device_id = device_id
|
|
68
|
+
else:
|
|
69
|
+
self.device_id = None
|
|
70
|
+
|
|
71
|
+
except (OSError, ModuleNotFoundError):
|
|
72
|
+
raise SoundDeviceError
|
|
73
|
+
if audio_format not in ["wav", "mp3", "webm"]:
|
|
74
|
+
raise ValueError(f"Unsupported audio format: {audio_format}")
|
|
75
|
+
self.audio_format = audio_format
|
|
76
|
+
|
|
77
|
+
def callback(self, indata, frames, time, status):
|
|
78
|
+
"""This is called (from a separate thread) for each audio block."""
|
|
79
|
+
import numpy as np
|
|
80
|
+
|
|
81
|
+
rms = np.sqrt(np.mean(indata**2))
|
|
82
|
+
self.max_rms = max(self.max_rms, rms)
|
|
83
|
+
self.min_rms = min(self.min_rms, rms)
|
|
84
|
+
|
|
85
|
+
rng = self.max_rms - self.min_rms
|
|
86
|
+
if rng > 0.001:
|
|
87
|
+
self.pct = (rms - self.min_rms) / rng
|
|
88
|
+
else:
|
|
89
|
+
self.pct = 0.5
|
|
90
|
+
|
|
91
|
+
self.q.put(indata.copy())
|
|
92
|
+
|
|
93
|
+
def get_prompt(self):
|
|
94
|
+
num = 10
|
|
95
|
+
if math.isnan(self.pct) or self.pct < self.threshold:
|
|
96
|
+
cnt = 0
|
|
97
|
+
else:
|
|
98
|
+
cnt = int(self.pct * 10)
|
|
99
|
+
|
|
100
|
+
bar = "░" * cnt + "█" * (num - cnt)
|
|
101
|
+
bar = bar[:num]
|
|
102
|
+
|
|
103
|
+
dur = time.time() - self.start_time
|
|
104
|
+
return f"Recording, press ENTER when done... {dur:.1f}sec {bar}"
|
|
105
|
+
|
|
106
|
+
def record_and_transcribe(self, history=None, language=None):
|
|
107
|
+
try:
|
|
108
|
+
return self.raw_record_and_transcribe(history, language)
|
|
109
|
+
except KeyboardInterrupt:
|
|
110
|
+
return
|
|
111
|
+
except SoundDeviceError as e:
|
|
112
|
+
print(f"Error: {e}")
|
|
113
|
+
print("Please ensure you have a working audio input device connected and try again.")
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
def raw_record_and_transcribe(self, history, language):
|
|
117
|
+
self.q = queue.Queue()
|
|
118
|
+
|
|
119
|
+
temp_wav = tempfile.mktemp(suffix=".wav")
|
|
120
|
+
|
|
121
|
+
try:
|
|
122
|
+
sample_rate = int(self.sd.query_devices(self.device_id, "input")["default_samplerate"])
|
|
123
|
+
except (TypeError, ValueError):
|
|
124
|
+
sample_rate = 16000 # fallback to 16kHz if unable to query device
|
|
125
|
+
except self.sd.PortAudioError:
|
|
126
|
+
raise SoundDeviceError(
|
|
127
|
+
"No audio input device detected. Please check your audio settings and try again."
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
self.start_time = time.time()
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
with self.sd.InputStream(
|
|
134
|
+
samplerate=sample_rate, channels=1, callback=self.callback, device=self.device_id
|
|
135
|
+
):
|
|
136
|
+
prompt(self.get_prompt, refresh_interval=0.1)
|
|
137
|
+
except self.sd.PortAudioError as err:
|
|
138
|
+
raise SoundDeviceError(f"Error accessing audio input device: {err}")
|
|
139
|
+
|
|
140
|
+
with sf.SoundFile(temp_wav, mode="x", samplerate=sample_rate, channels=1) as file:
|
|
141
|
+
while not self.q.empty():
|
|
142
|
+
file.write(self.q.get())
|
|
143
|
+
|
|
144
|
+
use_audio_format = self.audio_format
|
|
145
|
+
|
|
146
|
+
# Check file size and offer to convert to mp3 if too large
|
|
147
|
+
file_size = os.path.getsize(temp_wav)
|
|
148
|
+
if file_size > 24.9 * 1024 * 1024 and self.audio_format == "wav":
|
|
149
|
+
print("\nWarning: {temp_wav} is too large, switching to mp3 format.")
|
|
150
|
+
use_audio_format = "mp3"
|
|
151
|
+
|
|
152
|
+
filename = temp_wav
|
|
153
|
+
if use_audio_format != "wav":
|
|
154
|
+
try:
|
|
155
|
+
new_filename = tempfile.mktemp(suffix=f".{use_audio_format}")
|
|
156
|
+
audio = AudioSegment.from_wav(temp_wav)
|
|
157
|
+
audio.export(new_filename, format=use_audio_format)
|
|
158
|
+
os.remove(temp_wav)
|
|
159
|
+
filename = new_filename
|
|
160
|
+
except (CouldntDecodeError, CouldntEncodeError) as e:
|
|
161
|
+
print(f"Error converting audio: {e}")
|
|
162
|
+
except (OSError, FileNotFoundError) as e:
|
|
163
|
+
print(f"File system error during conversion: {e}")
|
|
164
|
+
except Exception as e:
|
|
165
|
+
print(f"Unexpected error during audio conversion: {e}")
|
|
166
|
+
|
|
167
|
+
with open(filename, "rb") as fh:
|
|
168
|
+
try:
|
|
169
|
+
transcript = litellm.transcription(
|
|
170
|
+
model="whisper-1", file=fh, prompt=history, language=language
|
|
171
|
+
)
|
|
172
|
+
except Exception as err:
|
|
173
|
+
print(f"Unable to transcribe {filename}: {err}")
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
if filename != temp_wav:
|
|
177
|
+
os.remove(filename)
|
|
178
|
+
|
|
179
|
+
text = transcript.text
|
|
180
|
+
return text
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
if __name__ == "__main__":
|
|
184
|
+
api_key = os.getenv("OPENAI_API_KEY")
|
|
185
|
+
if not api_key:
|
|
186
|
+
raise ValueError("Please set the OPENAI_API_KEY environment variable.")
|
|
187
|
+
print(Voice().record_and_transcribe())
|
aider/waiting.py
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Thread-based, killable spinner utility.
|
|
5
|
+
|
|
6
|
+
Use it like:
|
|
7
|
+
|
|
8
|
+
from aider.waiting import WaitingSpinner
|
|
9
|
+
|
|
10
|
+
spinner = WaitingSpinner("Waiting for LLM")
|
|
11
|
+
spinner.start()
|
|
12
|
+
... # long task
|
|
13
|
+
spinner.stop()
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import sys
|
|
17
|
+
import threading
|
|
18
|
+
import time
|
|
19
|
+
|
|
20
|
+
from rich.console import Console
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Spinner:
|
|
24
|
+
"""
|
|
25
|
+
Minimal spinner that scans a single marker back and forth across a line.
|
|
26
|
+
|
|
27
|
+
The animation is pre-rendered into a list of frames. If the terminal
|
|
28
|
+
cannot display unicode the frames are converted to plain ASCII.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
last_frame_idx = 0 # Class variable to store the last frame index
|
|
32
|
+
|
|
33
|
+
def __init__(self, text: str, width: int = 7):
|
|
34
|
+
self.text = text
|
|
35
|
+
self.start_time = time.time()
|
|
36
|
+
self.last_update = 0.0
|
|
37
|
+
self.visible = False
|
|
38
|
+
self.is_tty = sys.stdout.isatty()
|
|
39
|
+
self.console = Console()
|
|
40
|
+
|
|
41
|
+
# Pre-render the animation frames using pure ASCII so they will
|
|
42
|
+
# always display, even on very limited terminals.
|
|
43
|
+
ascii_frames = [
|
|
44
|
+
"#= ", # C1 C2 space(8)
|
|
45
|
+
"=# ", # C2 C1 space(8)
|
|
46
|
+
" =# ", # space(1) C2 C1 space(7)
|
|
47
|
+
" =# ", # space(2) C2 C1 space(6)
|
|
48
|
+
" =# ", # space(3) C2 C1 space(5)
|
|
49
|
+
" =# ", # space(4) C2 C1 space(4)
|
|
50
|
+
" =# ", # space(5) C2 C1 space(3)
|
|
51
|
+
" =# ", # space(6) C2 C1 space(2)
|
|
52
|
+
" =# ", # space(7) C2 C1 space(1)
|
|
53
|
+
" =#", # space(8) C2 C1
|
|
54
|
+
" #=", # space(8) C1 C2
|
|
55
|
+
" #= ", # space(7) C1 C2 space(1)
|
|
56
|
+
" #= ", # space(6) C1 C2 space(2)
|
|
57
|
+
" #= ", # space(5) C1 C2 space(3)
|
|
58
|
+
" #= ", # space(4) C1 C2 space(4)
|
|
59
|
+
" #= ", # space(3) C1 C2 space(5)
|
|
60
|
+
" #= ", # space(2) C1 C2 space(6)
|
|
61
|
+
" #= ", # space(1) C1 C2 space(7)
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
self.unicode_palette = "░█"
|
|
65
|
+
xlate_from, xlate_to = ("=#", self.unicode_palette)
|
|
66
|
+
|
|
67
|
+
# If unicode is supported, swap the ASCII chars for nicer glyphs.
|
|
68
|
+
if self._supports_unicode():
|
|
69
|
+
translation_table = str.maketrans(xlate_from, xlate_to)
|
|
70
|
+
frames = [f.translate(translation_table) for f in ascii_frames]
|
|
71
|
+
self.scan_char = xlate_to[xlate_from.find("#")]
|
|
72
|
+
else:
|
|
73
|
+
frames = ascii_frames
|
|
74
|
+
self.scan_char = "#"
|
|
75
|
+
|
|
76
|
+
# Bounce the scanner back and forth.
|
|
77
|
+
self.frames = frames
|
|
78
|
+
self.frame_idx = Spinner.last_frame_idx # Initialize from class variable
|
|
79
|
+
self.width = len(frames[0]) - 2 # number of chars between the brackets
|
|
80
|
+
self.animation_len = len(frames[0])
|
|
81
|
+
self.last_display_len = 0 # Length of the last spinner line (frame + text)
|
|
82
|
+
|
|
83
|
+
def _supports_unicode(self) -> bool:
|
|
84
|
+
if not self.is_tty:
|
|
85
|
+
return False
|
|
86
|
+
try:
|
|
87
|
+
out = self.unicode_palette
|
|
88
|
+
out += "\b" * len(self.unicode_palette)
|
|
89
|
+
out += " " * len(self.unicode_palette)
|
|
90
|
+
out += "\b" * len(self.unicode_palette)
|
|
91
|
+
sys.stdout.write(out)
|
|
92
|
+
sys.stdout.flush()
|
|
93
|
+
return True
|
|
94
|
+
except UnicodeEncodeError:
|
|
95
|
+
return False
|
|
96
|
+
except Exception:
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
def _next_frame(self) -> str:
|
|
100
|
+
frame = self.frames[self.frame_idx]
|
|
101
|
+
self.frame_idx = (self.frame_idx + 1) % len(self.frames)
|
|
102
|
+
Spinner.last_frame_idx = self.frame_idx # Update class variable
|
|
103
|
+
return frame
|
|
104
|
+
|
|
105
|
+
def step(self, text: str = None) -> None:
|
|
106
|
+
if text is not None:
|
|
107
|
+
self.text = text
|
|
108
|
+
|
|
109
|
+
if not self.is_tty:
|
|
110
|
+
return
|
|
111
|
+
|
|
112
|
+
now = time.time()
|
|
113
|
+
if not self.visible and now - self.start_time >= 0.5:
|
|
114
|
+
self.visible = True
|
|
115
|
+
self.last_update = 0.0
|
|
116
|
+
if self.is_tty:
|
|
117
|
+
self.console.show_cursor(False)
|
|
118
|
+
|
|
119
|
+
if not self.visible or now - self.last_update < 0.1:
|
|
120
|
+
return
|
|
121
|
+
|
|
122
|
+
self.last_update = now
|
|
123
|
+
frame_str = self._next_frame()
|
|
124
|
+
|
|
125
|
+
# Determine the maximum width for the spinner line
|
|
126
|
+
# Subtract 2 as requested, to leave a margin or prevent cursor wrapping issues
|
|
127
|
+
max_spinner_width = self.console.width - 2
|
|
128
|
+
if max_spinner_width < 0: # Handle extremely narrow terminals
|
|
129
|
+
max_spinner_width = 0
|
|
130
|
+
|
|
131
|
+
current_text_payload = f" {self.text}"
|
|
132
|
+
line_to_display = f"{frame_str}{current_text_payload}"
|
|
133
|
+
|
|
134
|
+
# Truncate the line if it's too long for the console width
|
|
135
|
+
if len(line_to_display) > max_spinner_width:
|
|
136
|
+
line_to_display = line_to_display[:max_spinner_width]
|
|
137
|
+
|
|
138
|
+
len_line_to_display = len(line_to_display)
|
|
139
|
+
|
|
140
|
+
# Calculate padding to clear any remnants from a longer previous line
|
|
141
|
+
padding_to_clear = " " * max(0, self.last_display_len - len_line_to_display)
|
|
142
|
+
|
|
143
|
+
# Write the spinner frame, text, and any necessary clearing spaces
|
|
144
|
+
sys.stdout.write(f"\r{line_to_display}{padding_to_clear}")
|
|
145
|
+
self.last_display_len = len_line_to_display
|
|
146
|
+
|
|
147
|
+
# Calculate number of backspaces to position cursor at the scanner character
|
|
148
|
+
scan_char_abs_pos = frame_str.find(self.scan_char)
|
|
149
|
+
|
|
150
|
+
# Total characters written to the line (frame + text + padding)
|
|
151
|
+
total_chars_written_on_line = len_line_to_display + len(padding_to_clear)
|
|
152
|
+
|
|
153
|
+
# num_backspaces will be non-positive if scan_char_abs_pos is beyond
|
|
154
|
+
# total_chars_written_on_line (e.g., if the scan char itself was truncated).
|
|
155
|
+
# (e.g., if the scan char itself was truncated).
|
|
156
|
+
# In such cases, (effectively) 0 backspaces are written,
|
|
157
|
+
# and the cursor stays at the end of the line.
|
|
158
|
+
num_backspaces = total_chars_written_on_line - scan_char_abs_pos
|
|
159
|
+
sys.stdout.write("\b" * num_backspaces)
|
|
160
|
+
sys.stdout.flush()
|
|
161
|
+
|
|
162
|
+
def end(self) -> None:
|
|
163
|
+
if self.visible and self.is_tty:
|
|
164
|
+
clear_len = self.last_display_len # Use the length of the last displayed content
|
|
165
|
+
sys.stdout.write("\r" + " " * clear_len + "\r")
|
|
166
|
+
sys.stdout.flush()
|
|
167
|
+
self.console.show_cursor(True)
|
|
168
|
+
self.visible = False
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class WaitingSpinner:
|
|
172
|
+
"""Background spinner that can be started/stopped safely."""
|
|
173
|
+
|
|
174
|
+
def __init__(self, text: str = "Waiting for LLM", delay: float = 0.15):
|
|
175
|
+
self.spinner = Spinner(text)
|
|
176
|
+
self.delay = delay
|
|
177
|
+
self._stop_event = threading.Event()
|
|
178
|
+
self._thread = threading.Thread(target=self._spin, daemon=True)
|
|
179
|
+
|
|
180
|
+
def _spin(self):
|
|
181
|
+
while not self._stop_event.is_set():
|
|
182
|
+
self.spinner.step()
|
|
183
|
+
time.sleep(self.delay)
|
|
184
|
+
self.spinner.end()
|
|
185
|
+
|
|
186
|
+
def start(self):
|
|
187
|
+
"""Start the spinner in a background thread."""
|
|
188
|
+
if not self._thread.is_alive():
|
|
189
|
+
self._thread.start()
|
|
190
|
+
|
|
191
|
+
def stop(self):
|
|
192
|
+
"""Request the spinner to stop and wait briefly for the thread to exit."""
|
|
193
|
+
self._stop_event.set()
|
|
194
|
+
if self._thread.is_alive():
|
|
195
|
+
self._thread.join(timeout=self.delay)
|
|
196
|
+
self.spinner.end()
|
|
197
|
+
|
|
198
|
+
# Allow use as a context-manager
|
|
199
|
+
def __enter__(self):
|
|
200
|
+
self.start()
|
|
201
|
+
return self
|
|
202
|
+
|
|
203
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
204
|
+
self.stop()
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def main():
|
|
208
|
+
spinner = Spinner("Running spinner...")
|
|
209
|
+
try:
|
|
210
|
+
for _ in range(100):
|
|
211
|
+
time.sleep(0.15)
|
|
212
|
+
spinner.step()
|
|
213
|
+
print("Success!")
|
|
214
|
+
except KeyboardInterrupt:
|
|
215
|
+
print("\nInterrupted by user.")
|
|
216
|
+
finally:
|
|
217
|
+
spinner.end()
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
if __name__ == "__main__":
|
|
221
|
+
main()
|