shrinkray 26.1.1.0__py3-none-any.whl → 26.2.4.1__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.
- shrinkray/subprocess/client.py +21 -1
- shrinkray/tui.py +70 -54
- {shrinkray-26.1.1.0.dist-info → shrinkray-26.2.4.1.dist-info}/METADATA +1 -1
- {shrinkray-26.1.1.0.dist-info → shrinkray-26.2.4.1.dist-info}/RECORD +8 -8
- {shrinkray-26.1.1.0.dist-info → shrinkray-26.2.4.1.dist-info}/WHEEL +1 -1
- {shrinkray-26.1.1.0.dist-info → shrinkray-26.2.4.1.dist-info}/entry_points.txt +0 -0
- {shrinkray-26.1.1.0.dist-info → shrinkray-26.2.4.1.dist-info}/licenses/LICENSE +0 -0
- {shrinkray-26.1.1.0.dist-info → shrinkray-26.2.4.1.dist-info}/top_level.txt +0 -0
shrinkray/subprocess/client.py
CHANGED
|
@@ -27,6 +27,7 @@ class SubprocessClient:
|
|
|
27
27
|
self._progress_queue: asyncio.Queue[ProgressUpdate] = asyncio.Queue()
|
|
28
28
|
self._reader_task: asyncio.Task | None = None
|
|
29
29
|
self._completed = False
|
|
30
|
+
self._closed = False
|
|
30
31
|
self._error_message: str | None = None
|
|
31
32
|
self._debug_mode = debug_mode
|
|
32
33
|
self._stderr_log_file: IO[str] | None = None
|
|
@@ -261,6 +262,17 @@ class SubprocessClient:
|
|
|
261
262
|
|
|
262
263
|
async def close(self) -> None:
|
|
263
264
|
"""Close the subprocess."""
|
|
265
|
+
if self._closed:
|
|
266
|
+
return
|
|
267
|
+
self._closed = True
|
|
268
|
+
|
|
269
|
+
# Cancel all pending futures first so any code awaiting send_command
|
|
270
|
+
# responses (e.g. cancel() in action_quit) is unblocked immediately.
|
|
271
|
+
for future in self._pending_responses.values():
|
|
272
|
+
if not future.done():
|
|
273
|
+
future.cancel()
|
|
274
|
+
self._pending_responses.clear()
|
|
275
|
+
|
|
264
276
|
if self._reader_task is not None:
|
|
265
277
|
self._reader_task.cancel()
|
|
266
278
|
try:
|
|
@@ -278,13 +290,21 @@ class SubprocessClient:
|
|
|
278
290
|
if self._process.returncode is None:
|
|
279
291
|
try:
|
|
280
292
|
self._process.terminate()
|
|
281
|
-
await asyncio.wait_for(self._process.wait(), timeout=
|
|
293
|
+
await asyncio.wait_for(self._process.wait(), timeout=2.0)
|
|
282
294
|
except TimeoutError:
|
|
283
295
|
self._process.kill()
|
|
284
296
|
await self._process.wait()
|
|
285
297
|
except ProcessLookupError:
|
|
286
298
|
pass # Process already exited
|
|
287
299
|
|
|
300
|
+
# Always await wait() to ensure the asyncio transport is fully
|
|
301
|
+
# finalized before the event loop closes. This prevents the
|
|
302
|
+
# "Event loop is closed" RuntimeError from BaseSubprocessTransport.__del__.
|
|
303
|
+
if self._process.returncode is not None:
|
|
304
|
+
await self._process.wait()
|
|
305
|
+
# Flush pending event loop callbacks (transport cleanup)
|
|
306
|
+
await asyncio.sleep(0)
|
|
307
|
+
|
|
288
308
|
# Close and remove the stderr log file
|
|
289
309
|
if self._stderr_log_file is not None:
|
|
290
310
|
try:
|
shrinkray/tui.py
CHANGED
|
@@ -13,7 +13,6 @@ from difflib import unified_diff
|
|
|
13
13
|
from typing import Literal, Protocol, cast
|
|
14
14
|
|
|
15
15
|
import humanize
|
|
16
|
-
from rich.markup import escape as escape_markup
|
|
17
16
|
from rich.text import Text
|
|
18
17
|
from textual import work
|
|
19
18
|
from textual.app import App, ComposeResult
|
|
@@ -46,6 +45,7 @@ from shrinkray.subprocess.protocol import (
|
|
|
46
45
|
|
|
47
46
|
ThemeMode = Literal["auto", "dark", "light"]
|
|
48
47
|
|
|
48
|
+
|
|
49
49
|
# Custom themes with true white/black backgrounds
|
|
50
50
|
SHRINKRAY_LIGHT_THEME = Theme(
|
|
51
51
|
name="shrinkray-light",
|
|
@@ -505,20 +505,20 @@ class ContentPreview(Static):
|
|
|
505
505
|
# Fallback based on common terminal height
|
|
506
506
|
return 30
|
|
507
507
|
|
|
508
|
-
def render(self) ->
|
|
508
|
+
def render(self) -> Text:
|
|
509
509
|
if not self.preview_content:
|
|
510
|
-
return "Loading..."
|
|
510
|
+
return Text("Loading...")
|
|
511
511
|
|
|
512
512
|
available_lines = self._get_available_lines()
|
|
513
513
|
|
|
514
514
|
if self.hex_mode:
|
|
515
|
-
return f"[Hex mode]\n{self.preview_content}"
|
|
515
|
+
return Text(f"[Hex mode]\n{self.preview_content}")
|
|
516
516
|
|
|
517
517
|
lines = self.preview_content.split("\n")
|
|
518
518
|
|
|
519
519
|
# For small files that fit, show full content
|
|
520
520
|
if len(lines) <= available_lines:
|
|
521
|
-
return self.preview_content
|
|
521
|
+
return Text(self.preview_content)
|
|
522
522
|
|
|
523
523
|
# For larger files, show diff if we have previous displayed content
|
|
524
524
|
if (
|
|
@@ -530,10 +530,10 @@ class ContentPreview(Static):
|
|
|
530
530
|
diff = list(unified_diff(prev_lines, curr_lines, lineterm=""))
|
|
531
531
|
if diff:
|
|
532
532
|
# Show as much diff as fits
|
|
533
|
-
return "\n".join(diff[:available_lines])
|
|
533
|
+
return Text("\n".join(diff[:available_lines]))
|
|
534
534
|
|
|
535
535
|
# No diff available, show truncated content
|
|
536
|
-
return (
|
|
536
|
+
return Text(
|
|
537
537
|
"\n".join(lines[:available_lines])
|
|
538
538
|
+ f"\n\n... ({len(lines) - available_lines} more lines)"
|
|
539
539
|
)
|
|
@@ -591,39 +591,46 @@ class OutputPreview(Static):
|
|
|
591
591
|
pass
|
|
592
592
|
return 30
|
|
593
593
|
|
|
594
|
-
def render(self) ->
|
|
594
|
+
def render(self) -> Text:
|
|
595
595
|
# Header line - use return_code to determine if test is running
|
|
596
596
|
# (return_code is None means still running, has value means completed)
|
|
597
597
|
if self.active_test_id is not None and self.last_return_code is None:
|
|
598
|
-
|
|
598
|
+
header_text = f"Test #{self.active_test_id} running..."
|
|
599
|
+
header_style = "green"
|
|
599
600
|
elif self.active_test_id is not None:
|
|
600
|
-
|
|
601
|
+
header_text = (
|
|
602
|
+
f"Test #{self.active_test_id} exited with code {self.last_return_code}"
|
|
603
|
+
)
|
|
604
|
+
header_style = "dim"
|
|
601
605
|
elif self._has_seen_output or self.output_content:
|
|
602
606
|
# Have seen output before - show without header
|
|
603
|
-
|
|
607
|
+
header_text = ""
|
|
608
|
+
header_style = ""
|
|
604
609
|
else:
|
|
605
|
-
|
|
610
|
+
header_text = "No test output yet..."
|
|
611
|
+
header_style = "dim"
|
|
606
612
|
|
|
607
613
|
if not self.output_content:
|
|
608
|
-
return
|
|
614
|
+
return Text(header_text, style=header_style)
|
|
609
615
|
|
|
610
616
|
available_lines = self._get_available_lines()
|
|
611
|
-
|
|
612
|
-
# (test output may contain characters like ^ that have special meaning)
|
|
613
|
-
escaped_content = escape_markup(self.output_content)
|
|
614
|
-
lines = escaped_content.split("\n")
|
|
615
|
-
|
|
616
|
-
# Build prefix (header + newline, or empty if no header)
|
|
617
|
-
prefix = f"{header}\n" if header else ""
|
|
617
|
+
lines = self.output_content.split("\n")
|
|
618
618
|
|
|
619
619
|
# Show tail of output (most recent lines)
|
|
620
|
-
if len(lines)
|
|
621
|
-
|
|
620
|
+
if len(lines) > available_lines:
|
|
621
|
+
truncated_lines = lines[-(available_lines):]
|
|
622
|
+
skipped = len(lines) - available_lines
|
|
623
|
+
content = f"... ({skipped} earlier lines)\n" + "\n".join(truncated_lines)
|
|
624
|
+
else:
|
|
625
|
+
content = self.output_content
|
|
622
626
|
|
|
623
|
-
#
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
+
# Build result with styled header and unstyled content
|
|
628
|
+
result = Text()
|
|
629
|
+
if header_text:
|
|
630
|
+
result.append(header_text, style=header_style)
|
|
631
|
+
result.append("\n")
|
|
632
|
+
result.append(content)
|
|
633
|
+
return result
|
|
627
634
|
|
|
628
635
|
|
|
629
636
|
class HelpScreen(ModalScreen[None]):
|
|
@@ -734,21 +741,20 @@ class ExpandedBoxModal(ModalScreen[None]):
|
|
|
734
741
|
self._content_widget_id = content_widget_id
|
|
735
742
|
self._file_path = file_path
|
|
736
743
|
|
|
737
|
-
def _read_file(self, file_path: str) ->
|
|
744
|
+
def _read_file(self, file_path: str) -> Text:
|
|
738
745
|
"""Read file content, decoding as text if possible."""
|
|
739
746
|
if not os.path.isfile(file_path):
|
|
740
|
-
return "
|
|
747
|
+
return Text("File not found", style="dim")
|
|
741
748
|
try:
|
|
742
749
|
with open(file_path, "rb") as f:
|
|
743
750
|
raw_content = f.read()
|
|
744
751
|
# Try to decode as text, fall back to hex display if binary
|
|
745
752
|
encoding, text = try_decode(raw_content)
|
|
746
753
|
if encoding is not None:
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
return "[Binary content - hex display]\n\n" + raw_content.hex()
|
|
754
|
+
return Text(text)
|
|
755
|
+
return Text("Binary content - hex display\n\n" + raw_content.hex())
|
|
750
756
|
except OSError:
|
|
751
|
-
return "
|
|
757
|
+
return Text("Error reading file", style="red")
|
|
752
758
|
|
|
753
759
|
def compose(self) -> ComposeResult:
|
|
754
760
|
with Vertical():
|
|
@@ -781,16 +787,16 @@ class ExpandedBoxModal(ModalScreen[None]):
|
|
|
781
787
|
return "Statistics not available"
|
|
782
788
|
return stats_displays[0].render()
|
|
783
789
|
|
|
784
|
-
def _get_file_content(self, app: "ShrinkRayApp") -> str:
|
|
790
|
+
def _get_file_content(self, app: "ShrinkRayApp") -> str | Text:
|
|
785
791
|
"""Get content from file or preview widget."""
|
|
786
792
|
if self._file_path:
|
|
787
793
|
return self._read_file(self._file_path)
|
|
788
794
|
content_previews = list(app.query("#content-preview").results(ContentPreview))
|
|
789
795
|
if not content_previews:
|
|
790
796
|
return "Content preview not available"
|
|
791
|
-
return content_previews[0].preview_content
|
|
797
|
+
return Text(content_previews[0].preview_content)
|
|
792
798
|
|
|
793
|
-
def _get_output_content(self, app: "ShrinkRayApp") -> str:
|
|
799
|
+
def _get_output_content(self, app: "ShrinkRayApp") -> str | Text:
|
|
794
800
|
"""Get output content from the output preview widget."""
|
|
795
801
|
output_previews = list(app.query("#output-preview").results(OutputPreview))
|
|
796
802
|
if not output_previews:
|
|
@@ -811,21 +817,29 @@ class ExpandedBoxModal(ModalScreen[None]):
|
|
|
811
817
|
)
|
|
812
818
|
has_seen_output = output_preview._has_seen_output
|
|
813
819
|
|
|
814
|
-
# Build header
|
|
820
|
+
# Build header with styling
|
|
815
821
|
if test_id is not None and return_code is None:
|
|
816
|
-
|
|
822
|
+
header_text = f"Test #{test_id} running..."
|
|
823
|
+
header_style = "green"
|
|
817
824
|
elif test_id is not None:
|
|
818
|
-
|
|
825
|
+
header_text = f"Test #{test_id} exited with code {return_code}"
|
|
826
|
+
header_style = "dim"
|
|
819
827
|
else:
|
|
820
|
-
|
|
828
|
+
header_text = ""
|
|
829
|
+
header_style = ""
|
|
821
830
|
|
|
822
831
|
if raw_content:
|
|
823
|
-
|
|
832
|
+
result = Text()
|
|
833
|
+
if header_text:
|
|
834
|
+
result.append(header_text, style=header_style)
|
|
835
|
+
result.append("\n\n")
|
|
836
|
+
result.append(raw_content)
|
|
837
|
+
return result
|
|
824
838
|
elif has_seen_output or test_id is not None:
|
|
825
839
|
# We've seen output before - show header only (no "No test output" message)
|
|
826
|
-
return
|
|
840
|
+
return Text(header_text, style=header_style)
|
|
827
841
|
else:
|
|
828
|
-
return "
|
|
842
|
+
return Text("No test output yet...", style="dim")
|
|
829
843
|
|
|
830
844
|
def on_mount(self) -> None:
|
|
831
845
|
"""Populate content from the source widget."""
|
|
@@ -1447,13 +1461,13 @@ class HistoryExplorerModal(ModalScreen[None]):
|
|
|
1447
1461
|
if os.path.isfile(output_path):
|
|
1448
1462
|
output_content = self._read_file(output_path)
|
|
1449
1463
|
else:
|
|
1450
|
-
output_content = "
|
|
1464
|
+
output_content = Text("No output captured", style="dim")
|
|
1451
1465
|
self.query_one(f"#{output_preview_id}", Static).update(output_content)
|
|
1452
1466
|
|
|
1453
|
-
def _read_file(self, file_path: str) ->
|
|
1467
|
+
def _read_file(self, file_path: str) -> Text:
|
|
1454
1468
|
"""Read file content, decoding as text if possible."""
|
|
1455
1469
|
if not os.path.isfile(file_path):
|
|
1456
|
-
return "
|
|
1470
|
+
return Text("File not found", style="dim")
|
|
1457
1471
|
try:
|
|
1458
1472
|
with open(file_path, "rb") as f:
|
|
1459
1473
|
raw_content = f.read()
|
|
@@ -1465,18 +1479,17 @@ class HistoryExplorerModal(ModalScreen[None]):
|
|
|
1465
1479
|
# Try to decode as text
|
|
1466
1480
|
encoding, text = try_decode(raw_content)
|
|
1467
1481
|
if encoding is not None:
|
|
1468
|
-
|
|
1469
|
-
text = escape_markup(text)
|
|
1482
|
+
result = Text(text)
|
|
1470
1483
|
if truncated:
|
|
1471
|
-
|
|
1472
|
-
return
|
|
1484
|
+
result.append("\n\n... (truncated)", style="dim")
|
|
1485
|
+
return result
|
|
1473
1486
|
# Binary content - hex display
|
|
1474
|
-
|
|
1487
|
+
result = Text("Binary content - hex display\n\n" + raw_content.hex())
|
|
1475
1488
|
if truncated:
|
|
1476
|
-
|
|
1477
|
-
return
|
|
1489
|
+
result.append("\n\n... (truncated)", style="dim")
|
|
1490
|
+
return result
|
|
1478
1491
|
except OSError:
|
|
1479
|
-
return "
|
|
1492
|
+
return Text("Error reading file", style="red")
|
|
1480
1493
|
|
|
1481
1494
|
def action_restart_from_here(self) -> None:
|
|
1482
1495
|
"""Restart reduction from the currently selected history point."""
|
|
@@ -1925,11 +1938,14 @@ class ShrinkRayApp(App[None]):
|
|
|
1925
1938
|
|
|
1926
1939
|
async def action_quit(self) -> None:
|
|
1927
1940
|
"""Quit the application with graceful cancellation."""
|
|
1941
|
+
self.update_status("Shutting down...")
|
|
1928
1942
|
if self._client and not self._completed:
|
|
1929
1943
|
try:
|
|
1930
|
-
await self._client.
|
|
1944
|
+
await self._client.close()
|
|
1931
1945
|
except Exception:
|
|
1932
1946
|
pass # Process may have already exited
|
|
1947
|
+
# Prevent double-close in run_reduction's finally block
|
|
1948
|
+
self._client = None
|
|
1933
1949
|
self.exit()
|
|
1934
1950
|
|
|
1935
1951
|
def action_show_pass_stats(self) -> None:
|
|
@@ -8,7 +8,7 @@ shrinkray/process.py,sha256=-eP8h5X0ESbkcTic8FFEzkd4-vwaZ0YI5tLxUR25L8U,1599
|
|
|
8
8
|
shrinkray/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
9
|
shrinkray/reducer.py,sha256=8CF_SxkfVMBxiikQKwv-rlrQawTLhQxy6QnVwIWWiws,18601
|
|
10
10
|
shrinkray/state.py,sha256=gDKz1jov84lEih-7jtYcYoWfIS2k2YtBtWuJmcAx6Is,38478
|
|
11
|
-
shrinkray/tui.py,sha256=
|
|
11
|
+
shrinkray/tui.py,sha256=40od9sbfOER-Pg_ZRL1MZ8XhOnH8r5axbtJcb827qPo,73924
|
|
12
12
|
shrinkray/ui.py,sha256=xuDUwU-MM3AetvwUB7bfzav0P_drUsBrKFPhON_Nr-k,2251
|
|
13
13
|
shrinkray/validation.py,sha256=piBCO-k9he_id6TWC4EHMK3GfuyPqRcNfkNJPVjxEaU,13366
|
|
14
14
|
shrinkray/work.py,sha256=GEZ14Kk3bvwUxAnACvY-wom2lVWaGrELMNxrDjv03dk,8110
|
|
@@ -23,12 +23,12 @@ shrinkray/passes/python.py,sha256=3WN1lZTf5oVL8FCTGomhrCuE04wIX9ocKcmFV86NMZA,68
|
|
|
23
23
|
shrinkray/passes/sat.py,sha256=OboY6jsKf6lph3pAFh535plvhNOVzEF8HJ66WEqsNm4,19483
|
|
24
24
|
shrinkray/passes/sequences.py,sha256=-5ajmMeHnS7onjjppbxLiP0F6mRSqiFI5DspBTj2x_M,2206
|
|
25
25
|
shrinkray/subprocess/__init__.py,sha256=qxZ19Nzizbm7H0MkKL38OqfP7U-VuOAvaqBVkmHFivY,375
|
|
26
|
-
shrinkray/subprocess/client.py,sha256=
|
|
26
|
+
shrinkray/subprocess/client.py,sha256=4nMFiHfqGSLQHTZ1jgt4dq2GUCBXwgTqvzPw6Z1N1q4,12199
|
|
27
27
|
shrinkray/subprocess/protocol.py,sha256=86sSxexQpPpr4W2C1y0V5Ddqoqb-1LfHa2wKjJSzmJA,7340
|
|
28
28
|
shrinkray/subprocess/worker.py,sha256=Spy2DjzJGHgFmr9PTxkd68q1REoymWcA4Ls4iDD36tE,31373
|
|
29
|
-
shrinkray-26.
|
|
30
|
-
shrinkray-26.
|
|
31
|
-
shrinkray-26.
|
|
32
|
-
shrinkray-26.
|
|
33
|
-
shrinkray-26.
|
|
34
|
-
shrinkray-26.
|
|
29
|
+
shrinkray-26.2.4.1.dist-info/licenses/LICENSE,sha256=iMKX79AuokJfIZUnGUARdUp30vVAoIPOJ7ek8TY63kk,1072
|
|
30
|
+
shrinkray-26.2.4.1.dist-info/METADATA,sha256=t4FlQASaioxkzPRRxdjaMxgd3JU_pFeQuHanZ3sOPFE,7836
|
|
31
|
+
shrinkray-26.2.4.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
32
|
+
shrinkray-26.2.4.1.dist-info/entry_points.txt,sha256=wIZvnGyOdVeaLTiv2klnSyTe-EKkkwn4SwHh9bmJ7qk,104
|
|
33
|
+
shrinkray-26.2.4.1.dist-info/top_level.txt,sha256=fLif8-rFoFOnf5h8-vs3ECkKNWQopTQh3xvl1s7pchQ,10
|
|
34
|
+
shrinkray-26.2.4.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|