deepy-cli 0.1.7__tar.gz → 0.1.9__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.
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/PKG-INFO +2 -2
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/README.md +1 -1
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/pyproject.toml +1 -1
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/__init__.py +1 -1
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/data/tools/shell.md +4 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/tools/builtin.py +123 -13
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/ui/prompt_input.py +141 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/__main__.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/cli.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/config/__init__.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/config/settings.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/data/__init__.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/data/tools/AskUserQuestion.md +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/data/tools/WebFetch.md +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/data/tools/WebSearch.md +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/data/tools/__init__.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/data/tools/edit.md +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/data/tools/modify.md +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/data/tools/read.md +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/data/tools/write.md +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/errors.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/llm/__init__.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/llm/agent.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/llm/compaction.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/llm/context.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/llm/events.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/llm/model_capabilities.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/llm/provider.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/llm/replay.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/llm/runner.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/llm/thinking.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/prompts/__init__.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/prompts/compact.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/prompts/rules.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/prompts/runtime_context.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/prompts/system.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/prompts/tool_docs.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/sessions/__init__.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/sessions/jsonl.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/sessions/manager.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/skills.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/status.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/tools/__init__.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/tools/agents.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/tools/file_state.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/tools/result.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/tools/shell_utils.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/ui/__init__.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/ui/app.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/ui/ask_user_question.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/ui/exit_summary.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/ui/loading_text.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/ui/markdown.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/ui/message_view.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/ui/model_picker.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/ui/prompt_buffer.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/ui/session_list.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/ui/session_picker.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/ui/slash_commands.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/ui/styles.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/ui/terminal.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/ui/theme_picker.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/ui/thinking_state.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/ui/welcome.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/update_check.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/usage.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/utils/__init__.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/utils/debug_logger.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/utils/error_logger.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/utils/json.py +0 -0
- {deepy_cli-0.1.7 → deepy_cli-0.1.9}/src/deepy/utils/notify.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: deepy-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.9
|
|
4
4
|
Summary: Deepy - Vibe coding for DeepSeek models in your terminal
|
|
5
5
|
Keywords: deepseek,coding-agent,terminal,cli,agents
|
|
6
6
|
Author: kirineko
|
|
@@ -238,5 +238,5 @@ assets live outside the package directory and are not included in the wheel.
|
|
|
238
238
|
|
|
239
239
|
## Release Status
|
|
240
240
|
|
|
241
|
-
Deepy `0.1.
|
|
241
|
+
Deepy `0.1.9` is released through GitHub and PyPI. Standalone binaries and npm
|
|
242
242
|
wrappers can be added later, but the primary distribution is the Python CLI.
|
|
@@ -210,5 +210,5 @@ assets live outside the package directory and are not included in the wheel.
|
|
|
210
210
|
|
|
211
211
|
## Release Status
|
|
212
212
|
|
|
213
|
-
Deepy `0.1.
|
|
213
|
+
Deepy `0.1.9` is released through GitHub and PyPI. Standalone binaries and npm
|
|
214
214
|
wrappers can be added later, but the primary distribution is the Python CLI.
|
|
@@ -9,5 +9,9 @@ Use the runtime context's command dialect and path style: PowerShell uses
|
|
|
9
9
|
PowerShell commands and Windows paths, `cmd` uses cmd syntax, and `posix` uses
|
|
10
10
|
POSIX shell syntax.
|
|
11
11
|
|
|
12
|
+
On Windows PowerShell, Python child processes run with UTF-8 I/O defaults for
|
|
13
|
+
the command invocation; do not ask users to run `chcp` or change their
|
|
14
|
+
PowerShell profile for Unicode output.
|
|
15
|
+
|
|
12
16
|
Runs in the session cwd, preserves cwd between calls when supported, and returns
|
|
13
17
|
stdout/stderr JSON with cwd, exit-code, and shell metadata.
|
|
@@ -141,6 +141,13 @@ class MatchOccurrence:
|
|
|
141
141
|
end_line: int
|
|
142
142
|
|
|
143
143
|
|
|
144
|
+
@dataclass(frozen=True)
|
|
145
|
+
class EditMatchResult:
|
|
146
|
+
matches: list[MatchOccurrence]
|
|
147
|
+
matched_text: str
|
|
148
|
+
matched_via: str
|
|
149
|
+
|
|
150
|
+
|
|
144
151
|
@dataclass(frozen=True)
|
|
145
152
|
class ClosestMatch:
|
|
146
153
|
text: str
|
|
@@ -208,6 +215,7 @@ class ShellInvocation:
|
|
|
208
215
|
shell_path: str
|
|
209
216
|
args: list[str]
|
|
210
217
|
runtime_environment: RuntimeEnvironment
|
|
218
|
+
env: dict[str, str] | None = None
|
|
211
219
|
|
|
212
220
|
|
|
213
221
|
def _find_occurrences(text: str, needle: str, scope: tuple[int, int]) -> list[MatchOccurrence]:
|
|
@@ -231,6 +239,63 @@ def _find_occurrences(text: str, needle: str, scope: tuple[int, int]) -> list[Ma
|
|
|
231
239
|
search_index = found + len(needle)
|
|
232
240
|
|
|
233
241
|
|
|
242
|
+
def _find_edit_occurrences(
|
|
243
|
+
text: str,
|
|
244
|
+
needle: str,
|
|
245
|
+
scope: tuple[int, int],
|
|
246
|
+
line_endings: str,
|
|
247
|
+
*,
|
|
248
|
+
matched_via: str = "exact",
|
|
249
|
+
) -> EditMatchResult | None:
|
|
250
|
+
matches = _find_occurrences(text, needle, scope)
|
|
251
|
+
if matches:
|
|
252
|
+
return EditMatchResult(matches=matches, matched_text=needle, matched_via=matched_via)
|
|
253
|
+
normalized_needle = _normalize_line_endings(needle, line_endings)
|
|
254
|
+
if normalized_needle == needle:
|
|
255
|
+
return None
|
|
256
|
+
normalized_matches = _find_occurrences(text, normalized_needle, scope)
|
|
257
|
+
if not normalized_matches:
|
|
258
|
+
return None
|
|
259
|
+
return EditMatchResult(
|
|
260
|
+
matches=normalized_matches,
|
|
261
|
+
matched_text=normalized_needle,
|
|
262
|
+
matched_via=_line_ending_matched_via(matched_via),
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _find_loose_escape_edit_occurrences(
|
|
267
|
+
text: str,
|
|
268
|
+
needle: str,
|
|
269
|
+
scope: tuple[int, int],
|
|
270
|
+
line_endings: str,
|
|
271
|
+
) -> list[tuple[MatchOccurrence, float, str, str]]:
|
|
272
|
+
matches = [
|
|
273
|
+
(occurrence, score, matched_text, "loose_escape")
|
|
274
|
+
for occurrence, score, matched_text in _find_loose_escape_occurrences(text, needle, scope)
|
|
275
|
+
]
|
|
276
|
+
normalized_needle = _normalize_line_endings(needle, line_endings)
|
|
277
|
+
if normalized_needle == needle:
|
|
278
|
+
return matches
|
|
279
|
+
matches.extend(
|
|
280
|
+
(
|
|
281
|
+
occurrence,
|
|
282
|
+
score,
|
|
283
|
+
matched_text,
|
|
284
|
+
"loose_escape_line_endings",
|
|
285
|
+
)
|
|
286
|
+
for occurrence, score, matched_text in _find_loose_escape_occurrences(
|
|
287
|
+
text,
|
|
288
|
+
normalized_needle,
|
|
289
|
+
scope,
|
|
290
|
+
)
|
|
291
|
+
)
|
|
292
|
+
return matches
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _line_ending_matched_via(matched_via: str) -> str:
|
|
296
|
+
return "line_endings" if matched_via == "exact" else f"{matched_via}_line_endings"
|
|
297
|
+
|
|
298
|
+
|
|
234
299
|
def _offset_to_line(text: str, offset: int) -> int:
|
|
235
300
|
if offset <= 0:
|
|
236
301
|
return 1
|
|
@@ -1308,12 +1373,14 @@ class ToolRuntime:
|
|
|
1308
1373
|
return ToolResult.error_result(name, error or "File is not writable.").to_json()
|
|
1309
1374
|
text_metadata = _read_text_metadata(target)
|
|
1310
1375
|
text = text_metadata.content
|
|
1376
|
+
line_endings = text_metadata.line_endings
|
|
1311
1377
|
scope = _edit_scope(text, snippet)
|
|
1312
|
-
|
|
1313
|
-
|
|
1378
|
+
match_result = _find_edit_occurrences(text, old, scope, line_endings)
|
|
1379
|
+
matches = match_result.matches if match_result is not None else []
|
|
1380
|
+
matched_via = match_result.matched_via if match_result is not None else "exact"
|
|
1314
1381
|
replacement_new = new
|
|
1315
1382
|
if not matches:
|
|
1316
|
-
loose_matches =
|
|
1383
|
+
loose_matches = _find_loose_escape_edit_occurrences(text, old, scope, line_endings)
|
|
1317
1384
|
if len(loose_matches) == 1 and loose_matches[0][1] == 1.0:
|
|
1318
1385
|
corrected = _correct_escaped_strings_with_llm(
|
|
1319
1386
|
self.settings,
|
|
@@ -1324,14 +1391,20 @@ class ToolRuntime:
|
|
|
1324
1391
|
)
|
|
1325
1392
|
if corrected is not None:
|
|
1326
1393
|
corrected_old, corrected_new = corrected
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1394
|
+
corrected_match_result = _find_edit_occurrences(
|
|
1395
|
+
text,
|
|
1396
|
+
corrected_old,
|
|
1397
|
+
scope,
|
|
1398
|
+
line_endings,
|
|
1399
|
+
matched_via="llm_escape_correction",
|
|
1400
|
+
)
|
|
1401
|
+
if corrected_match_result is not None:
|
|
1402
|
+
matches = corrected_match_result.matches
|
|
1330
1403
|
replacement_new = corrected_new
|
|
1331
|
-
matched_via =
|
|
1404
|
+
matched_via = corrected_match_result.matched_via
|
|
1332
1405
|
if not matches:
|
|
1333
1406
|
matches = [loose_matches[0][0]]
|
|
1334
|
-
matched_via =
|
|
1407
|
+
matched_via = loose_matches[0][3]
|
|
1335
1408
|
if not matches:
|
|
1336
1409
|
closest_match = _find_closest_match(text, old, scope)
|
|
1337
1410
|
metadata = {"scope": _format_scope_metadata(target, snippet, scope, text)}
|
|
@@ -1363,7 +1436,6 @@ class ToolRuntime:
|
|
|
1363
1436
|
),
|
|
1364
1437
|
},
|
|
1365
1438
|
).to_json()
|
|
1366
|
-
line_endings = text_metadata.line_endings
|
|
1367
1439
|
normalized_new = _normalize_line_endings(replacement_new, line_endings)
|
|
1368
1440
|
updated = _apply_replacements(text, matches, normalized_new, replace_all)
|
|
1369
1441
|
_write_text_with_encoding(target, updated, text_metadata.encoding)
|
|
@@ -1399,6 +1471,7 @@ class ToolRuntime:
|
|
|
1399
1471
|
process = subprocess.Popen(
|
|
1400
1472
|
[shell_invocation.shell_path, *shell_invocation.args],
|
|
1401
1473
|
cwd=self.cwd,
|
|
1474
|
+
env=shell_invocation.env,
|
|
1402
1475
|
text=True,
|
|
1403
1476
|
stdout=stdout_file,
|
|
1404
1477
|
stderr=stderr_file,
|
|
@@ -1750,8 +1823,7 @@ def _read_text_preserving_newlines(path: Path) -> str:
|
|
|
1750
1823
|
def _read_text_metadata(path: Path) -> TextFileMetadata:
|
|
1751
1824
|
data = path.read_bytes()
|
|
1752
1825
|
encoding = _detect_text_encoding(data)
|
|
1753
|
-
|
|
1754
|
-
text = data.decode(python_encoding, errors="replace")
|
|
1826
|
+
text = data.decode(_python_text_encoding(encoding), errors="replace")
|
|
1755
1827
|
return TextFileMetadata(
|
|
1756
1828
|
content=text,
|
|
1757
1829
|
encoding=encoding,
|
|
@@ -1762,12 +1834,32 @@ def _read_text_metadata(path: Path) -> TextFileMetadata:
|
|
|
1762
1834
|
def _detect_text_encoding(data: bytes) -> str:
|
|
1763
1835
|
if len(data) >= 2 and data[0] == 0xFF and data[1] == 0xFE:
|
|
1764
1836
|
return "utf16le"
|
|
1837
|
+
if data.startswith(b"\xef\xbb\xbf"):
|
|
1838
|
+
return "utf8-sig"
|
|
1839
|
+
try:
|
|
1840
|
+
data.decode("utf-8", errors="strict")
|
|
1841
|
+
return "utf8"
|
|
1842
|
+
except UnicodeDecodeError:
|
|
1843
|
+
pass
|
|
1844
|
+
try:
|
|
1845
|
+
data.decode("gb18030", errors="strict")
|
|
1846
|
+
return "gb18030"
|
|
1847
|
+
except UnicodeDecodeError:
|
|
1848
|
+
return "utf8"
|
|
1849
|
+
|
|
1850
|
+
|
|
1851
|
+
def _python_text_encoding(encoding: str) -> str:
|
|
1852
|
+
if encoding == "utf16le":
|
|
1853
|
+
return "utf-16"
|
|
1854
|
+
if encoding == "utf8-sig":
|
|
1855
|
+
return "utf-8-sig"
|
|
1856
|
+
if encoding == "gb18030":
|
|
1857
|
+
return "gb18030"
|
|
1765
1858
|
return "utf8"
|
|
1766
1859
|
|
|
1767
1860
|
|
|
1768
1861
|
def _write_text_with_encoding(path: Path, content: str, encoding: str) -> None:
|
|
1769
|
-
|
|
1770
|
-
path.write_text(content, encoding=python_encoding)
|
|
1862
|
+
path.write_text(content, encoding=_python_text_encoding(encoding))
|
|
1771
1863
|
|
|
1772
1864
|
|
|
1773
1865
|
def _coerce_write_content(path: Path, content: object) -> tuple[str, dict[str, object], str | None]:
|
|
@@ -2034,25 +2126,41 @@ def _build_shell_command(
|
|
|
2034
2126
|
platform_name=platform_name,
|
|
2035
2127
|
os_name=os_name,
|
|
2036
2128
|
)
|
|
2129
|
+
process_env = _build_shell_process_env(runtime_environment, env)
|
|
2037
2130
|
if runtime_environment.command_dialect == "powershell":
|
|
2038
2131
|
return ShellInvocation(
|
|
2039
2132
|
shell_path=resolved_shell,
|
|
2040
2133
|
args=_build_powershell_args(command, marker),
|
|
2041
2134
|
runtime_environment=runtime_environment,
|
|
2135
|
+
env=process_env,
|
|
2042
2136
|
)
|
|
2043
2137
|
if runtime_environment.command_dialect == "cmd":
|
|
2044
2138
|
return ShellInvocation(
|
|
2045
2139
|
shell_path=resolved_shell,
|
|
2046
2140
|
args=_build_cmd_args(command, marker),
|
|
2047
2141
|
runtime_environment=runtime_environment,
|
|
2142
|
+
env=process_env,
|
|
2048
2143
|
)
|
|
2049
2144
|
return ShellInvocation(
|
|
2050
2145
|
shell_path=resolved_shell,
|
|
2051
2146
|
args=_build_posix_shell_args(command, marker, resolved_shell),
|
|
2052
2147
|
runtime_environment=runtime_environment,
|
|
2148
|
+
env=process_env,
|
|
2053
2149
|
)
|
|
2054
2150
|
|
|
2055
2151
|
|
|
2152
|
+
def _build_shell_process_env(
|
|
2153
|
+
runtime_environment: RuntimeEnvironment,
|
|
2154
|
+
env: dict[str, str] | None = None,
|
|
2155
|
+
) -> dict[str, str] | None:
|
|
2156
|
+
if runtime_environment.os_family != "windows":
|
|
2157
|
+
return dict(env) if env is not None else None
|
|
2158
|
+
process_env = dict(os.environ if env is None else env)
|
|
2159
|
+
process_env.setdefault("PYTHONUTF8", "1")
|
|
2160
|
+
process_env.setdefault("PYTHONIOENCODING", "utf-8")
|
|
2161
|
+
return process_env
|
|
2162
|
+
|
|
2163
|
+
|
|
2056
2164
|
def _build_posix_shell_args(command: str, marker: str, shell_path: str) -> list[str]:
|
|
2057
2165
|
normalized_command = rewrite_windows_null_redirect(command)
|
|
2058
2166
|
parts = [
|
|
@@ -2073,6 +2181,8 @@ def _build_posix_shell_args(command: str, marker: str, shell_path: str) -> list[
|
|
|
2073
2181
|
def _build_powershell_args(command: str, marker: str) -> list[str]:
|
|
2074
2182
|
script = "\n".join(
|
|
2075
2183
|
[
|
|
2184
|
+
"$OutputEncoding = [System.Text.UTF8Encoding]::new($false)",
|
|
2185
|
+
"[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new($false)",
|
|
2076
2186
|
"$global:LASTEXITCODE = $null",
|
|
2077
2187
|
"try {",
|
|
2078
2188
|
command,
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from dataclasses import dataclass
|
|
4
4
|
from pathlib import Path
|
|
5
|
+
import sys
|
|
5
6
|
from typing import Callable
|
|
6
7
|
from unicodedata import normalize
|
|
7
8
|
|
|
@@ -32,6 +33,8 @@ SHIFT_ENTER_SEQUENCES = (
|
|
|
32
33
|
"\x1b[27;2;13~", # xterm modified-key format.
|
|
33
34
|
"\x1b[13;2u", # Kitty/fixterms CSI-u format, used by modern terminals.
|
|
34
35
|
)
|
|
36
|
+
_WINDOWS_SHIFT_ENTER_PATCH_ATTR = "_deepy_shift_enter_patched"
|
|
37
|
+
_WINDOWS_SHIFT_ENTER_VT100_PATCH_ATTR = "_deepy_shift_enter_vt100_patched"
|
|
35
38
|
|
|
36
39
|
|
|
37
40
|
@dataclass(frozen=True)
|
|
@@ -90,6 +93,12 @@ def build_prompt_key_bindings(
|
|
|
90
93
|
def _(event) -> None: # pragma: no cover - prompt_toolkit calls this callback
|
|
91
94
|
event.current_buffer.insert_text("\n")
|
|
92
95
|
|
|
96
|
+
if is_windows_newline_fallback_enabled():
|
|
97
|
+
|
|
98
|
+
@bindings.add("c-j")
|
|
99
|
+
def _(event) -> None: # pragma: no cover - prompt_toolkit calls this callback
|
|
100
|
+
event.current_buffer.insert_text("\n")
|
|
101
|
+
|
|
93
102
|
return bindings
|
|
94
103
|
|
|
95
104
|
|
|
@@ -102,6 +111,138 @@ def install_shift_enter_key_sequence_overrides() -> None:
|
|
|
102
111
|
prefix_cache = getattr(vt100_parser, "_IS_PREFIX_OF_LONGER_MATCH_CACHE", None)
|
|
103
112
|
if hasattr(prefix_cache, "clear"):
|
|
104
113
|
prefix_cache.clear()
|
|
114
|
+
install_windows_shift_enter_key_sequence_override()
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def install_windows_shift_enter_key_sequence_override(
|
|
118
|
+
*,
|
|
119
|
+
platform_name: str | None = None,
|
|
120
|
+
console_input_reader_cls: type | None = None,
|
|
121
|
+
vt100_console_input_reader_cls: type | None = None,
|
|
122
|
+
event_types: object | None = None,
|
|
123
|
+
key_event_record_cls: type | None = None,
|
|
124
|
+
) -> bool:
|
|
125
|
+
resolved_platform = platform_name or sys.platform
|
|
126
|
+
if not resolved_platform.startswith("win"):
|
|
127
|
+
return False
|
|
128
|
+
if console_input_reader_cls is None and vt100_console_input_reader_cls is None:
|
|
129
|
+
try:
|
|
130
|
+
from prompt_toolkit.input import win32
|
|
131
|
+
except (AssertionError, ImportError):
|
|
132
|
+
return False
|
|
133
|
+
console_input_reader_cls = win32.ConsoleInputReader
|
|
134
|
+
vt100_console_input_reader_cls = getattr(win32, "Vt100ConsoleInputReader", None)
|
|
135
|
+
if event_types is None:
|
|
136
|
+
event_types = getattr(win32, "EventTypes", None)
|
|
137
|
+
if key_event_record_cls is None:
|
|
138
|
+
key_event_record_cls = getattr(win32, "KEY_EVENT_RECORD", None)
|
|
139
|
+
patched = False
|
|
140
|
+
if console_input_reader_cls is not None:
|
|
141
|
+
patched = _patch_windows_console_input_reader(console_input_reader_cls) or patched
|
|
142
|
+
if vt100_console_input_reader_cls is not None:
|
|
143
|
+
patched = (
|
|
144
|
+
_patch_windows_vt100_console_input_reader(
|
|
145
|
+
vt100_console_input_reader_cls,
|
|
146
|
+
event_types=event_types,
|
|
147
|
+
key_event_record_cls=key_event_record_cls,
|
|
148
|
+
)
|
|
149
|
+
or patched
|
|
150
|
+
)
|
|
151
|
+
return patched
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _patch_windows_console_input_reader(console_input_reader_cls: type) -> bool:
|
|
155
|
+
if getattr(console_input_reader_cls, _WINDOWS_SHIFT_ENTER_PATCH_ATTR, False):
|
|
156
|
+
return True
|
|
157
|
+
|
|
158
|
+
from prompt_toolkit.key_binding.key_processor import KeyPress
|
|
159
|
+
|
|
160
|
+
original_handler = console_input_reader_cls._event_to_key_presses
|
|
161
|
+
shift_pressed = getattr(console_input_reader_cls, "SHIFT_PRESSED", 0x0010)
|
|
162
|
+
|
|
163
|
+
def patched_event_to_key_presses(self, ev):
|
|
164
|
+
key_presses = original_handler(self, ev)
|
|
165
|
+
if _is_windows_shift_enter_key_press(ev, key_presses, shift_pressed=shift_pressed):
|
|
166
|
+
return [KeyPress(Keys.Escape, ""), key_presses[0]]
|
|
167
|
+
return key_presses
|
|
168
|
+
|
|
169
|
+
setattr(
|
|
170
|
+
console_input_reader_cls,
|
|
171
|
+
"_deepy_original_event_to_key_presses",
|
|
172
|
+
original_handler,
|
|
173
|
+
)
|
|
174
|
+
console_input_reader_cls._event_to_key_presses = patched_event_to_key_presses
|
|
175
|
+
setattr(console_input_reader_cls, _WINDOWS_SHIFT_ENTER_PATCH_ATTR, True)
|
|
176
|
+
return True
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _patch_windows_vt100_console_input_reader(
|
|
180
|
+
vt100_console_input_reader_cls: type,
|
|
181
|
+
*,
|
|
182
|
+
event_types: object | None,
|
|
183
|
+
key_event_record_cls: type | None,
|
|
184
|
+
) -> bool:
|
|
185
|
+
if event_types is None or key_event_record_cls is None:
|
|
186
|
+
return False
|
|
187
|
+
if getattr(vt100_console_input_reader_cls, _WINDOWS_SHIFT_ENTER_VT100_PATCH_ATTR, False):
|
|
188
|
+
return True
|
|
189
|
+
|
|
190
|
+
original_get_keys = vt100_console_input_reader_cls._get_keys
|
|
191
|
+
shift_pressed = getattr(vt100_console_input_reader_cls, "SHIFT_PRESSED", 0x0010)
|
|
192
|
+
|
|
193
|
+
def patched_get_keys(self, read, input_records):
|
|
194
|
+
for index in range(read.value):
|
|
195
|
+
input_record = input_records[index]
|
|
196
|
+
if input_record.EventType not in event_types:
|
|
197
|
+
continue
|
|
198
|
+
event_name = event_types[input_record.EventType]
|
|
199
|
+
event = getattr(input_record.Event, event_name)
|
|
200
|
+
if not isinstance(event, key_event_record_cls) or not event.KeyDown:
|
|
201
|
+
continue
|
|
202
|
+
if _is_windows_shift_enter_event(event, shift_pressed=shift_pressed):
|
|
203
|
+
yield SHIFT_ENTER_SEQUENCES[0]
|
|
204
|
+
continue
|
|
205
|
+
u_char = event.uChar.UnicodeChar
|
|
206
|
+
if u_char != "\x00":
|
|
207
|
+
yield u_char
|
|
208
|
+
|
|
209
|
+
setattr(
|
|
210
|
+
vt100_console_input_reader_cls,
|
|
211
|
+
"_deepy_original_get_keys",
|
|
212
|
+
original_get_keys,
|
|
213
|
+
)
|
|
214
|
+
vt100_console_input_reader_cls._get_keys = patched_get_keys
|
|
215
|
+
setattr(vt100_console_input_reader_cls, _WINDOWS_SHIFT_ENTER_VT100_PATCH_ATTR, True)
|
|
216
|
+
return True
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _is_windows_shift_enter_key_press(
|
|
220
|
+
ev,
|
|
221
|
+
key_presses: list,
|
|
222
|
+
*,
|
|
223
|
+
shift_pressed: int,
|
|
224
|
+
) -> bool:
|
|
225
|
+
if not key_presses or len(key_presses) != 1:
|
|
226
|
+
return False
|
|
227
|
+
control_key_state = getattr(ev, "ControlKeyState", 0)
|
|
228
|
+
if not control_key_state & shift_pressed:
|
|
229
|
+
return False
|
|
230
|
+
key = getattr(key_presses[0], "key", None)
|
|
231
|
+
return key in {Keys.ControlM, Keys.ControlJ, Keys.Enter}
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _is_windows_shift_enter_event(ev, *, shift_pressed: int) -> bool:
|
|
235
|
+
control_key_state = getattr(ev, "ControlKeyState", 0)
|
|
236
|
+
if not control_key_state & shift_pressed:
|
|
237
|
+
return False
|
|
238
|
+
u_char = getattr(getattr(ev, "uChar", None), "UnicodeChar", "")
|
|
239
|
+
virtual_key_code = getattr(ev, "VirtualKeyCode", None)
|
|
240
|
+
return u_char in {"\r", "\n"} or virtual_key_code == 13
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def is_windows_newline_fallback_enabled(platform_name: str | None = None) -> bool:
|
|
244
|
+
resolved_platform = platform_name or sys.platform
|
|
245
|
+
return resolved_platform.startswith("win")
|
|
105
246
|
|
|
106
247
|
|
|
107
248
|
def prompt_for_input(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|