pixelification 1.2.1__tar.gz → 1.2.5__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 (30) hide show
  1. {pixelification-1.2.1 → pixelification-1.2.5}/PKG-INFO +37 -1
  2. {pixelification-1.2.1 → pixelification-1.2.5}/README.md +36 -0
  3. pixelification-1.2.5/requirements.txt +20 -0
  4. {pixelification-1.2.1 → pixelification-1.2.5}/src/pixelification/main.py +282 -3
  5. {pixelification-1.2.1 → pixelification-1.2.5}/uv.lock +11 -6
  6. pixelification-1.2.1/requirements.txt +0 -6
  7. {pixelification-1.2.1 → pixelification-1.2.5}/.github/workflows/publish.yml +0 -0
  8. {pixelification-1.2.1 → pixelification-1.2.5}/.gitignore +0 -0
  9. {pixelification-1.2.1 → pixelification-1.2.5}/.python-version +0 -0
  10. {pixelification-1.2.1 → pixelification-1.2.5}/Cargo.lock +0 -0
  11. {pixelification-1.2.1 → pixelification-1.2.5}/Cargo.toml +0 -0
  12. {pixelification-1.2.1 → pixelification-1.2.5}/main.py +0 -0
  13. {pixelification-1.2.1 → pixelification-1.2.5}/pyproject.toml +0 -0
  14. {pixelification-1.2.1 → pixelification-1.2.5}/src/app/mod.rs +0 -0
  15. {pixelification-1.2.1 → pixelification-1.2.5}/src/config/mod.rs +0 -0
  16. {pixelification-1.2.1 → pixelification-1.2.5}/src/drag/mod.rs +0 -0
  17. {pixelification-1.2.1 → pixelification-1.2.5}/src/events/mod.rs +0 -0
  18. {pixelification-1.2.1 → pixelification-1.2.5}/src/main.rs +0 -0
  19. {pixelification-1.2.1 → pixelification-1.2.5}/src/pixelification/__init__.py +0 -0
  20. {pixelification-1.2.1 → pixelification-1.2.5}/src/pixelification/runtime.py +0 -0
  21. {pixelification-1.2.1 → pixelification-1.2.5}/src/state/mod.rs +0 -0
  22. {pixelification-1.2.1 → pixelification-1.2.5}/src/ui/animated.rs +0 -0
  23. {pixelification-1.2.1 → pixelification-1.2.5}/src/ui/bitmap.rs +0 -0
  24. {pixelification-1.2.1 → pixelification-1.2.5}/src/ui/brush.rs +0 -0
  25. {pixelification-1.2.1 → pixelification-1.2.5}/src/ui/color.rs +0 -0
  26. {pixelification-1.2.1 → pixelification-1.2.5}/src/ui/font.rs +0 -0
  27. {pixelification-1.2.1 → pixelification-1.2.5}/src/ui/mod.rs +0 -0
  28. {pixelification-1.2.1 → pixelification-1.2.5}/src/ui/render.rs +0 -0
  29. {pixelification-1.2.1 → pixelification-1.2.5}/src/webview/mod.rs +0 -0
  30. {pixelification-1.2.1 → pixelification-1.2.5}/src/win32.rs +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pixelification
3
- Version: 1.2.1
3
+ Version: 1.2.5
4
4
  Summary: Pixel Rearrangement Tool — Keyboard-Navigated Terminal UI
5
5
  Requires-Python: >=3.10
6
6
  Requires-Dist: numpy>=1.20.0
@@ -84,6 +84,31 @@ Rearrange pixels from a **source image** to approximate the layout of a **target
84
84
 
85
85
  ---
86
86
 
87
+ ### ▸ ASCII Art Mode
88
+
89
+ Turn any image into **terminal ASCII art** — a scrollable character-based rendering using 10 luminance levels with auto-contrast and Floyd-Steinberg dithering.
90
+
91
+ ```
92
+ ┌─────────────────────────────────────────────────────────────┐
93
+ │ @@@@@@%####****+==--::.. Shift+arrows scroll │
94
+ │ @@@%%%###***+++==--::.. PgUp/PgDn jump │
95
+ │ @%@%%%####***+++==--::.. 10 levels + dither │
96
+ │ @@@%%%###***+++==--::.. │
97
+ └─────────────────────────────────────────────────────────────┘
98
+ ```
99
+
100
+ **How it works:**
101
+
102
+ | Step | What happens |
103
+ |------|-------------|
104
+ | **Read** | Image loaded via OpenCV, converted to luminance using Rec. 601 `(0.299R + 0.587G + 0.114B)` |
105
+ | **Contrast** | Histogram stretched to full 0–255 range for maximum tonal depth |
106
+ | **Resize** | Aspect-corrected resize (0.55 char ratio) to fit terminal width |
107
+ | **Dither** | Floyd-Steinberg error diffusion distributes quantization error across neighboring pixels |
108
+ | **Map** | Each pixel's brightness mapped to one of 10 characters `@%#*+=-:. ` sorted by ink coverage |
109
+
110
+ ---
111
+
87
112
  ### ▸ Video Mode
88
113
 
89
114
  Rearrange every frame of a **source video** (or a still image looped as a video) to match the frames of a **target video**.
@@ -171,6 +196,7 @@ A keyboard-navigated terminal UI opens immediately:
171
196
 
172
197
  ● Rearrange Images sort pixels between two images
173
198
  ○ Rearrange Videos sort frames between two videos
199
+ ○ ASCII convert images to ASCII art
174
200
  ○ Quit exit the application
175
201
  ```
176
202
 
@@ -181,6 +207,8 @@ A keyboard-navigated terminal UI opens immediately:
181
207
  | `↑` `↓` | Navigate menu items |
182
208
  | `Enter` | Select highlighted item |
183
209
  | `1`–`N` | Jump directly to item *N* |
210
+ | `Shift`+`↑` `↓` `←` `→` | Scroll ASCII art (vertical / horizontal) |
211
+ | `PgUp` / `PgDn` | Scroll ASCII art by one page |
184
212
  | `q` / `Esc` | Quit |
185
213
 
186
214
  ### Image Mode
@@ -199,6 +227,14 @@ A keyboard-navigated terminal UI opens immediately:
199
227
  4. Result plays in an OpenCV window — loops until `ESC`/`q` or window close
200
228
  5. Click **"Save Result Video"** to export
201
229
 
230
+ ### ASCII Art Mode
231
+
232
+ 1. Select an image file
233
+ 2. Press **"Run ASCII Conversion"** — the art renders instantly in the terminal
234
+ 3. Use `Shift+arrows` to scroll, `PgUp`/`PgDn` to jump by page
235
+ 4. **"Copy to Clipboard"** pipes the full result to your system clipboard (`clip.exe`)
236
+ 5. **"Save Result"** writes a `.txt` file to the current directory
237
+
202
238
  ---
203
239
 
204
240
  ## ◈ Requirements
@@ -72,6 +72,31 @@ Rearrange pixels from a **source image** to approximate the layout of a **target
72
72
 
73
73
  ---
74
74
 
75
+ ### ▸ ASCII Art Mode
76
+
77
+ Turn any image into **terminal ASCII art** — a scrollable character-based rendering using 10 luminance levels with auto-contrast and Floyd-Steinberg dithering.
78
+
79
+ ```
80
+ ┌─────────────────────────────────────────────────────────────┐
81
+ │ @@@@@@%####****+==--::.. Shift+arrows scroll │
82
+ │ @@@%%%###***+++==--::.. PgUp/PgDn jump │
83
+ │ @%@%%%####***+++==--::.. 10 levels + dither │
84
+ │ @@@%%%###***+++==--::.. │
85
+ └─────────────────────────────────────────────────────────────┘
86
+ ```
87
+
88
+ **How it works:**
89
+
90
+ | Step | What happens |
91
+ |------|-------------|
92
+ | **Read** | Image loaded via OpenCV, converted to luminance using Rec. 601 `(0.299R + 0.587G + 0.114B)` |
93
+ | **Contrast** | Histogram stretched to full 0–255 range for maximum tonal depth |
94
+ | **Resize** | Aspect-corrected resize (0.55 char ratio) to fit terminal width |
95
+ | **Dither** | Floyd-Steinberg error diffusion distributes quantization error across neighboring pixels |
96
+ | **Map** | Each pixel's brightness mapped to one of 10 characters `@%#*+=-:. ` sorted by ink coverage |
97
+
98
+ ---
99
+
75
100
  ### ▸ Video Mode
76
101
 
77
102
  Rearrange every frame of a **source video** (or a still image looped as a video) to match the frames of a **target video**.
@@ -159,6 +184,7 @@ A keyboard-navigated terminal UI opens immediately:
159
184
 
160
185
  ● Rearrange Images sort pixels between two images
161
186
  ○ Rearrange Videos sort frames between two videos
187
+ ○ ASCII convert images to ASCII art
162
188
  ○ Quit exit the application
163
189
  ```
164
190
 
@@ -169,6 +195,8 @@ A keyboard-navigated terminal UI opens immediately:
169
195
  | `↑` `↓` | Navigate menu items |
170
196
  | `Enter` | Select highlighted item |
171
197
  | `1`–`N` | Jump directly to item *N* |
198
+ | `Shift`+`↑` `↓` `←` `→` | Scroll ASCII art (vertical / horizontal) |
199
+ | `PgUp` / `PgDn` | Scroll ASCII art by one page |
172
200
  | `q` / `Esc` | Quit |
173
201
 
174
202
  ### Image Mode
@@ -187,6 +215,14 @@ A keyboard-navigated terminal UI opens immediately:
187
215
  4. Result plays in an OpenCV window — loops until `ESC`/`q` or window close
188
216
  5. Click **"Save Result Video"** to export
189
217
 
218
+ ### ASCII Art Mode
219
+
220
+ 1. Select an image file
221
+ 2. Press **"Run ASCII Conversion"** — the art renders instantly in the terminal
222
+ 3. Use `Shift+arrows` to scroll, `PgUp`/`PgDn` to jump by page
223
+ 4. **"Copy to Clipboard"** pipes the full result to your system clipboard (`clip.exe`)
224
+ 5. **"Save Result"** writes a `.txt` file to the current directory
225
+
190
226
  ---
191
227
 
192
228
  ## ◈ Requirements
@@ -0,0 +1,20 @@
1
+ prompt-toolkit>=3.0.0
2
+ opencv-python>=4.0.0
3
+ numpy>=1.20.0
4
+
5
+ # ── Developer Notes ─────────────────────────────────────────────────
6
+ #
7
+ # pip install -r requirements.txt → base deps only (no GPU)
8
+ # pip install -e . → base deps only (no GPU)
9
+ # pip install -e ".[cuda]" → includes CuPy + NVIDIA libs
10
+ #
11
+ # uv sync → base deps only
12
+ # uv sync --extra cuda → includes CuPy + NVIDIA libs
13
+ # uv sync --all-extras → includes CuPy + NVIDIA libs
14
+ #
15
+ # CuPy / NVIDIA packages are ONLY installed when the 'cuda' extra is
16
+ # explicitly requested. Users without an NVIDIA GPU should NOT use
17
+ # the [cuda] extra.
18
+ #
19
+ # ── Optional — CUDA acceleration (NVIDIA GPU required) ──────────────
20
+ # cupy-cuda13x[ctk]
@@ -211,7 +211,7 @@ def select_file(title: str = "Select File", file_type: str = "image") -> str | N
211
211
 
212
212
  @dataclass
213
213
  class State:
214
- screen: str = "main" # "main" | "image" | "video"
214
+ screen: str = "main" # "main" | "image" | "video" | "ascii"
215
215
  source: str = ""
216
216
  target: str = ""
217
217
  status: str = "Ready"
@@ -224,10 +224,14 @@ class State:
224
224
  result_video_path: str = ""
225
225
  using_accelerator: bool = HAS_CUPY
226
226
  acceleration_status: str = CUPY_STATUS_TEXT
227
+ result_ascii: str = ""
228
+ scroll_x: int = 0
229
+ scroll_y: int = 0
227
230
 
228
231
  MENU_MAIN = [
229
232
  ("Rearrange Images", "sort pixels between two images"),
230
233
  ("Rearrange Videos", "sort frames between two videos"),
234
+ ("ASCII", "convert images to ASCII art"),
231
235
  ("Quit", "exit the application"),
232
236
  ]
233
237
 
@@ -249,9 +253,18 @@ class State:
249
253
  ("Quit", "exit the application"),
250
254
  ]
251
255
 
256
+ MENU_ASCII = [
257
+ ("Select Image", "choose an image to convert"),
258
+ ("Run ASCII Conversion", "convert the selected image to ASCII art"),
259
+ ("Copy to Clipboard", "copy the ASCII art to the system clipboard"),
260
+ ("Save Result", "save the ASCII art to a .txt file"),
261
+ ("Back to Main Menu", "return to mode selection"),
262
+ ("Quit", "exit the application"),
263
+ ]
264
+
252
265
  @property
253
266
  def menu(self):
254
- return {"main": self.MENU_MAIN, "image": self.MENU_IMAGE, "video": self.MENU_VIDEO}[self.screen]
267
+ return {"main": self.MENU_MAIN, "image": self.MENU_IMAGE, "video": self.MENU_VIDEO, "ascii": self.MENU_ASCII}[self.screen]
255
268
 
256
269
 
257
270
  # ── Rearrangement Engine ─────────────────────────────────────────────
@@ -617,6 +630,68 @@ def rearrange_video(source_path: str, target_path: str, state: State) -> None:
617
630
  state.done = True
618
631
 
619
632
 
633
+ # ── ASCII Art Engine ─────────────────────────────────────────────────
634
+
635
+ ASCII_CHARS = "@%#*+=-:. "
636
+
637
+
638
+ def _auto_contrast(img: np.ndarray) -> np.ndarray:
639
+ lo, hi = int(img.min()), int(img.max())
640
+ if hi - lo < 1:
641
+ return img
642
+ return ((img.astype(np.float32) - lo) / (hi - lo) * 255).clip(0, 255).astype(np.uint8)
643
+
644
+
645
+ def _floyd_steinberg(gray: np.ndarray, levels: int) -> np.ndarray:
646
+ h, w = gray.shape
647
+ out = gray.astype(np.float32).copy()
648
+ step = 255.0 / (levels - 1)
649
+ for y in range(h):
650
+ for x in range(w):
651
+ old = out[y, x]
652
+ quantized = round(old / step) * step
653
+ out[y, x] = quantized
654
+ err = old - quantized
655
+ if x + 1 < w:
656
+ out[y, x + 1] += err * 7 / 16
657
+ if y + 1 < h:
658
+ if x > 0:
659
+ out[y + 1, x - 1] += err * 3 / 16
660
+ out[y + 1, x] += err * 5 / 16
661
+ if x + 1 < w:
662
+ out[y + 1, x + 1] += err * 1 / 16
663
+ return out.clip(0, 255).astype(np.uint8)
664
+
665
+
666
+ def image_to_ascii(path: str, width: int = 120, dither: bool = True) -> str:
667
+ bgr = cv2.imread(path, cv2.IMREAD_COLOR)
668
+ if bgr is None:
669
+ return ""
670
+
671
+ gray = (0.299 * bgr[:, :, 2] + 0.587 * bgr[:, :, 1] + 0.114 * bgr[:, :, 0]).astype(np.uint8)
672
+
673
+ gray = _auto_contrast(gray)
674
+
675
+ h, w = gray.shape[:2]
676
+ aspect = h / w
677
+ height = max(int(width * aspect * 0.55), 1)
678
+ resized = cv2.resize(gray, (width, height), interpolation=cv2.INTER_LANCZOS4)
679
+
680
+ ramp = ASCII_CHARS
681
+ ramp_len = len(ramp)
682
+
683
+ if dither:
684
+ resized = _floyd_steinberg(resized, ramp_len)
685
+
686
+ idx = (resized.astype(np.float32) / 255 * (ramp_len - 1)).round().astype(np.int32)
687
+ idx = np.clip(idx, 0, ramp_len - 1)
688
+
689
+ lines = []
690
+ for row in idx:
691
+ lines.append("".join(ramp[i] for i in row))
692
+ return "\n".join(lines)
693
+
694
+
620
695
  # ── TUI Application ──────────────────────────────────────────────────
621
696
 
622
697
  class PixelTUI:
@@ -663,6 +738,44 @@ class PixelTUI:
663
738
  self.state.cursor = idx
664
739
  self._dispatch(idx)
665
740
 
741
+ @kb.add("s-up")
742
+ def _(event):
743
+ if self.state.screen == "ascii" and self.state.result_ascii and not self.state.running:
744
+ self.state.scroll_y = max(0, self.state.scroll_y - 1)
745
+ self._invalidate()
746
+
747
+ @kb.add("s-down")
748
+ def _(event):
749
+ if self.state.screen == "ascii" and self.state.result_ascii and not self.state.running:
750
+ self.state.scroll_y += 1
751
+ self._invalidate()
752
+
753
+ @kb.add("s-left")
754
+ def _(event):
755
+ if self.state.screen == "ascii" and self.state.result_ascii and not self.state.running:
756
+ self.state.scroll_x = max(0, self.state.scroll_x - 4)
757
+ self._invalidate()
758
+
759
+ @kb.add("s-right")
760
+ def _(event):
761
+ if self.state.screen == "ascii" and self.state.result_ascii and not self.state.running:
762
+ self.state.scroll_x += 4
763
+ self._invalidate()
764
+
765
+ @kb.add("pageup")
766
+ def _(event):
767
+ if self.state.screen == "ascii" and self.state.result_ascii and not self.state.running:
768
+ jump = (self._app.output.get_size().rows - 12 if self._app else 20)
769
+ self.state.scroll_y = max(0, self.state.scroll_y - jump)
770
+ self._invalidate()
771
+
772
+ @kb.add("pagedown")
773
+ def _(event):
774
+ if self.state.screen == "ascii" and self.state.result_ascii and not self.state.running:
775
+ jump = (self._app.output.get_size().rows - 12 if self._app else 20)
776
+ self.state.scroll_y += jump
777
+ self._invalidate()
778
+
666
779
  @kb.add("escape")
667
780
  @kb.add("q")
668
781
  def _(event):
@@ -680,7 +793,8 @@ class PixelTUI:
680
793
  if s == "main":
681
794
  {0: self._enter_image_mode,
682
795
  1: self._enter_video_mode,
683
- 2: self._quit}[idx]()
796
+ 2: self._enter_ascii_mode,
797
+ 3: self._quit}[idx]()
684
798
  elif s == "image":
685
799
  {0: self._select_source,
686
800
  1: self._select_target,
@@ -695,6 +809,13 @@ class PixelTUI:
695
809
  3: self._save_result_video,
696
810
  4: self._back_to_main,
697
811
  5: self._quit}[idx]()
812
+ elif s == "ascii":
813
+ {0: self._select_source_ascii,
814
+ 1: self._run_ascii,
815
+ 2: self._copy_result_ascii,
816
+ 3: self._save_result_ascii,
817
+ 4: self._back_to_main,
818
+ 5: self._quit}[idx]()
698
819
 
699
820
  # ── Actions ──────────────────────────────────────────────────
700
821
 
@@ -836,6 +957,95 @@ class PixelTUI:
836
957
  if self._app:
837
958
  asyncio.create_task(waiter())
838
959
 
960
+ def _enter_ascii_mode(self):
961
+ self.state.screen = "ascii"
962
+ self.state.cursor = 0
963
+ self.state.scroll_x = 0
964
+ self.state.scroll_y = 0
965
+ self.state.status = "Select an image to convert to ASCII art"
966
+ self.state.status_style = "status-info"
967
+ self._invalidate()
968
+
969
+ def _select_source_ascii(self):
970
+ path = select_file("Select Image for ASCII", "image")
971
+ if path:
972
+ self.state.source = path
973
+ self.state.status = f"Image: {Path(path).name}"
974
+ self.state.status_style = "status"
975
+ else:
976
+ self.state.status = "Selection cancelled"
977
+ self.state.status_style = "status-info"
978
+ self._invalidate()
979
+
980
+ def _run_ascii(self):
981
+ if not self.state.source:
982
+ self.state.status = "Select an image first!"
983
+ self.state.status_style = "status-error"
984
+ self._invalidate()
985
+ return
986
+
987
+ self.state.status = "Converting to ASCII art\u2026"
988
+ self.state.status_style = "status-warn"
989
+ self._invalidate()
990
+
991
+ self.state.scroll_x = 0
992
+ self.state.scroll_y = 0
993
+ width = min(120, max(40, self._app.output.get_size().columns - 4 if self._app else 120))
994
+
995
+ try:
996
+ result = image_to_ascii(self.state.source, width)
997
+ if not result:
998
+ self.state.status = "Failed to read image"
999
+ self.state.status_style = "status-error"
1000
+ else:
1001
+ self.state.result_ascii = result
1002
+ self.state.done = True
1003
+ self.state.status = f"ASCII art generated ({len(result.split(chr(10)))} rows \u00d7 {width} cols)"
1004
+ self.state.status_style = "status"
1005
+ except Exception as e:
1006
+ self.state.status = f"Error: {e}"
1007
+ self.state.status_style = "status-error"
1008
+ self._invalidate()
1009
+
1010
+ def _save_result_ascii(self):
1011
+ if not self.state.result_ascii:
1012
+ self.state.status = "No result to save \u2014 run conversion first!"
1013
+ self.state.status_style = "status-error"
1014
+ self._invalidate()
1015
+ return
1016
+
1017
+ src_stem = Path(self.state.source).stem
1018
+ out_name = f"{src_stem}_ascii.txt"
1019
+ out_path = Path.cwd() / out_name
1020
+ try:
1021
+ out_path.write_text(self.state.result_ascii, encoding="utf-8")
1022
+ self.state.status = f"Saved to {out_path}"
1023
+ self.state.status_style = "status"
1024
+ except Exception as e:
1025
+ self.state.status = f"Save error: {e}"
1026
+ self.state.status_style = "status-error"
1027
+ self._invalidate()
1028
+
1029
+ def _copy_result_ascii(self):
1030
+ if not self.state.result_ascii:
1031
+ self.state.status = "No result to copy \u2014 run conversion first!"
1032
+ self.state.status_style = "status-error"
1033
+ self._invalidate()
1034
+ return
1035
+ try:
1036
+ import subprocess
1037
+ subprocess.run(
1038
+ ["clip"], text=True, input=self.state.result_ascii,
1039
+ creationflags=0x08000000,
1040
+ )
1041
+ size = len(self.state.result_ascii)
1042
+ self.state.status = f"Copied {size} characters to clipboard"
1043
+ self.state.status_style = "status"
1044
+ except Exception as e:
1045
+ self.state.status = f"Clipboard error: {e}"
1046
+ self.state.status_style = "status-error"
1047
+ self._invalidate()
1048
+
839
1049
  def _refresh_info(self):
840
1050
  s = self.state
841
1051
  parts = []
@@ -950,6 +1160,75 @@ class PixelTUI:
950
1160
  push("#585858 italic", "\n")
951
1161
  push("#585858 italic", f" Hardware acceleration: {accel_text}")
952
1162
  push("", "\n")
1163
+ elif s.screen == "ascii":
1164
+ push("bold #00d787", " \u25a0 Pixel Rearrangement Tool")
1165
+ push("bold #5f87ff", " \u2014 [ ASCII Art Mode ]")
1166
+ push("", "\n")
1167
+ push("#3a3a3a", " " + "\u2501" * 55)
1168
+ push("", "\n")
1169
+ push("bold #5f87ff", "\n Image: ")
1170
+ push("#87afff" if s.source else "#585858 italic",
1171
+ s.source if s.source else "\u2014 not selected \u2014")
1172
+ push("", "\n\n")
1173
+
1174
+ if s.result_ascii:
1175
+ all_lines = s.result_ascii.split("\n")
1176
+ total_rows = len(all_lines)
1177
+ total_cols = max(len(l) for l in all_lines) if all_lines else 0
1178
+
1179
+ vp_rows = max(3, (self._app.output.get_size().rows - 18
1180
+ if self._app else 10))
1181
+ vp_cols = (self._app.output.get_size().columns - 5
1182
+ if self._app else 76)
1183
+
1184
+ s.scroll_y = max(0, min(s.scroll_y, total_rows - vp_rows))
1185
+ s.scroll_x = max(0, min(s.scroll_x, total_cols - vp_cols))
1186
+
1187
+ scroll_ind_y = (" \u2191" if s.scroll_y > 0 else " ") + \
1188
+ (" \u2193" if s.scroll_y + vp_rows < total_rows else " ")
1189
+ scroll_ind_x = ("\u2190" if s.scroll_x > 0 else " ") + \
1190
+ ("\u2192" if s.scroll_x + vp_cols < total_cols else " ")
1191
+ info = f"{total_rows}\u00d7{total_cols} {scroll_ind_y} {scroll_ind_x}"
1192
+ push("#6c6c6c", f" {info}\n")
1193
+ push("", " " + "\u2500" * min(vp_cols, total_cols) + "\n")
1194
+ for line in all_lines[s.scroll_y:s.scroll_y + vp_rows]:
1195
+ slice = line[s.scroll_x:s.scroll_x + vp_cols]
1196
+ push("", " " + slice + "\n")
1197
+ push("", " " + "\u2500" * min(vp_cols, total_cols) + "\n")
1198
+ push("#585858 italic", " Shift+arrows scroll \u2022 PgUp/PgDn jump\n")
1199
+ elif s.done:
1200
+ push("status-warn", " Conversion produced no output.\n")
1201
+
1202
+ push("#3a3a3a", " " + "\u2500" * 55)
1203
+ push("", "\n")
1204
+
1205
+ for i, (label, desc) in enumerate(s.menu):
1206
+ cursor = "\u25cf" if i == s.cursor else "\u25cb"
1207
+ sel = i == s.cursor
1208
+ is_run = i == 1
1209
+ is_copy = i == 2
1210
+ is_save = i == 3
1211
+ disabled = (is_run and not s.source) or (is_copy and not s.done) or (is_save and not s.done)
1212
+ st = ("bold #000000 bg:#00d787" if sel else
1213
+ "#585858 italic" if disabled else
1214
+ "bold #ffffff")
1215
+ push(st, f" {cursor} {label} ")
1216
+ push("", " ")
1217
+ push("#3a3a3a" if disabled else "#6c6c6c", f"{desc}\n")
1218
+
1219
+ push("", "\n")
1220
+ push("#3a3a3a", " " + "\u2500" * 55)
1221
+ push("", "\n ")
1222
+
1223
+ c = {"status": "#5faf5f", "status-error": "#ff5f5f",
1224
+ "status-warn": "#ffaf5f", "status-info": "#878787 italic"}
1225
+ push(c.get(s.status_style, "#878787 italic"), s.status)
1226
+ push("", "\n")
1227
+
1228
+ n = len(s.menu)
1229
+ help_extra = " \u2022 Shift+arrows \u2022 PgUp/Dn jump" if s.result_ascii else ""
1230
+ push("#585858 italic", f"\u2191\u2193 navigate \u2022 Enter select \u2022 1-{n} shortcut{help_extra} \u2022 q quit")
1231
+ push("#585858 italic", "\n")
953
1232
  else:
954
1233
  mode_label = "Image Mode" if s.screen == "image" else "Video Mode"
955
1234
  push("bold #00d787", f" \u25a0 Pixel Rearrangement Tool")
@@ -58,9 +58,9 @@ name = "cupy-cuda13x"
58
58
  version = "14.1.0"
59
59
  source = { registry = "https://pypi.org/simple" }
60
60
  dependencies = [
61
- { name = "cuda-pathfinder", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
62
- { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version < '3.11' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform == 'win32')" },
63
- { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "(python_full_version >= '3.11' and sys_platform == 'linux') or (python_full_version >= '3.11' and sys_platform == 'win32')" },
61
+ { name = "cuda-pathfinder" },
62
+ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
63
+ { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
64
64
  ]
65
65
  wheels = [
66
66
  { url = "https://files.pythonhosted.org/packages/18/e9/19185f325905eada94ed96f649a2b92336dcb6caa62a5adac514bb2f0de5/cupy_cuda13x-14.1.0-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:80f169592f5bbf7cb411ce4eeda315d06addb1a3cb14ed668e9d2ba287e3e4d4", size = 72670820, upload-time = "2026-05-23T01:11:17.547Z" },
@@ -84,7 +84,7 @@ wheels = [
84
84
 
85
85
  [package.optional-dependencies]
86
86
  ctk = [
87
- { name = "cuda-toolkit", extra = ["cublas", "cudart", "cufft", "curand", "cusolver", "cusparse", "nvrtc"], marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
87
+ { name = "cuda-toolkit", extra = ["cublas", "cudart", "cufft", "curand", "cusolver", "cusparse", "nvrtc"] },
88
88
  ]
89
89
 
90
90
  [[package]]
@@ -353,20 +353,25 @@ wheels = [
353
353
  name = "pixelification"
354
354
  source = { editable = "." }
355
355
  dependencies = [
356
- { name = "cupy-cuda13x", extra = ["ctk"], marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
357
356
  { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
358
357
  { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
359
358
  { name = "opencv-python" },
360
359
  { name = "prompt-toolkit" },
361
360
  ]
362
361
 
362
+ [package.optional-dependencies]
363
+ cuda = [
364
+ { name = "cupy-cuda13x", extra = ["ctk"] },
365
+ ]
366
+
363
367
  [package.metadata]
364
368
  requires-dist = [
365
- { name = "cupy-cuda13x", extras = ["ctk"], marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
369
+ { name = "cupy-cuda13x", extras = ["ctk"], marker = "extra == 'cuda'" },
366
370
  { name = "numpy", specifier = ">=1.20.0" },
367
371
  { name = "opencv-python", specifier = ">=4.0.0" },
368
372
  { name = "prompt-toolkit", specifier = ">=3.0.0" },
369
373
  ]
374
+ provides-extras = ["cuda"]
370
375
 
371
376
  [[package]]
372
377
  name = "prompt-toolkit"
@@ -1,6 +0,0 @@
1
- prompt-toolkit>=3.0.0
2
- opencv-python>=4.0.0
3
- numpy>=1.20.0
4
-
5
- # Optional — CUDA acceleration (NVIDIA GPU required)
6
- # cupy-cuda13x[ctk]
File without changes