karnel-termux 4.7.6 → 4.7.7

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 (81) hide show
  1. package/README.md +230 -225
  2. package/install.sh +30 -9
  3. package/karnel/bin/karnel +2 -2
  4. package/karnel/cli/commands/backup.sh +10 -2
  5. package/karnel/cli/commands/brain.sh +28 -23
  6. package/karnel/cli/commands/cleanup.sh +10 -4
  7. package/karnel/cli/commands/deploy.sh +53 -0
  8. package/karnel/cli/commands/doctor/code.sh +225 -0
  9. package/karnel/cli/commands/doctor/code_detect.sh +184 -0
  10. package/karnel/cli/commands/doctor/code_exec.sh +278 -0
  11. package/karnel/cli/commands/doctor/code_langs.sh +229 -0
  12. package/karnel/cli/commands/doctor/fixes.sh +63 -46
  13. package/karnel/cli/commands/doctor.sh +222 -80
  14. package/karnel/cli/commands/env.sh +2 -0
  15. package/karnel/cli/commands/ia.sh +12 -2
  16. package/karnel/cli/commands/init.sh +17 -9
  17. package/karnel/cli/commands/install.sh +90 -54
  18. package/karnel/cli/commands/list.sh +36 -0
  19. package/karnel/cli/commands/pg.sh +21 -6
  20. package/karnel/cli/commands/reinstall.sh +99 -0
  21. package/karnel/cli/commands/restore.sh +43 -3
  22. package/karnel/cli/commands/search.sh +2 -2
  23. package/karnel/cli/commands/update.sh +6 -1
  24. package/karnel/cli/commands/upgrade.sh +8 -1
  25. package/karnel/cli/commands/voice.sh +2 -1
  26. package/karnel/cli/karnel.sh +11 -5
  27. package/karnel/modules/games.sh +11 -0
  28. package/karnel/tools/ai/all.sh +1 -0
  29. package/karnel/tools/ai/copilot-termux/README.md +40 -0
  30. package/karnel/tools/ai/copilot-termux/install.sh +80 -0
  31. package/karnel/tools/ai/copilot-termux/package.json +20 -0
  32. package/karnel/tools/db/all.sh +2 -0
  33. package/karnel/tools/deploy/railway/install.sh +2 -2
  34. package/karnel/tools/dev/all.sh +8 -0
  35. package/karnel/tools/dev/applaunch/__pycache__/applaunch.cpython-314.pyc +0 -0
  36. package/karnel/tools/dev/applaunch/applaunch.py +434 -0
  37. package/karnel/tools/dev/applaunch/install.sh +51 -0
  38. package/karnel/tools/dev/fconv/__pycache__/fconv.cpython-314.pyc +0 -0
  39. package/karnel/tools/dev/fconv/fconv.py +749 -0
  40. package/karnel/tools/dev/fconv/install.sh +51 -0
  41. package/karnel/tools/dev/filecheck/__pycache__/filecheck.cpython-314.pyc +0 -0
  42. package/karnel/tools/dev/filecheck/filecheck.py +429 -0
  43. package/karnel/tools/dev/filecheck/install.sh +51 -0
  44. package/karnel/tools/dev/notes/__pycache__/notes.cpython-314.pyc +0 -0
  45. package/karnel/tools/dev/notes/install.sh +51 -0
  46. package/karnel/tools/dev/notes/notes.py +849 -0
  47. package/karnel/tools/dev/passman/__pycache__/passman.cpython-314.pyc +0 -0
  48. package/karnel/tools/dev/passman/install.sh +51 -0
  49. package/karnel/tools/dev/passman/passman.py +536 -0
  50. package/karnel/tools/dev/splash/__pycache__/splash.cpython-314.pyc +0 -0
  51. package/karnel/tools/dev/splash/install.sh +51 -0
  52. package/karnel/tools/dev/splash/splash.py +188 -0
  53. package/karnel/tools/dev/treex/install.sh +51 -0
  54. package/karnel/tools/dev/treex/treex.py +2036 -0
  55. package/karnel/tools/dev/websites/__pycache__/websites.cpython-314.pyc +0 -0
  56. package/karnel/tools/dev/websites/install.sh +51 -0
  57. package/karnel/tools/dev/websites/websites.py +844 -0
  58. package/karnel/tools/games/all.sh +62 -0
  59. package/karnel/tools/games/arcade/__pycache__/arcade.cpython-314.pyc +0 -0
  60. package/karnel/tools/games/arcade/arcade.py +193 -0
  61. package/karnel/tools/games/arcade/install.sh +51 -0
  62. package/karnel/tools/games/buzz/__pycache__/buzz.cpython-314.pyc +0 -0
  63. package/karnel/tools/games/buzz/buzz.py +1264 -0
  64. package/karnel/tools/games/buzz/install.sh +51 -0
  65. package/karnel/tools/games/ctfgod/__pycache__/ctfgod.cpython-314.pyc +0 -0
  66. package/karnel/tools/games/ctfgod/ctfgod.py +1576 -0
  67. package/karnel/tools/games/ctfgod/install.sh +51 -0
  68. package/karnel/tools/games/detective/__pycache__/detective.cpython-314.pyc +0 -0
  69. package/karnel/tools/games/detective/detective.py +2091 -0
  70. package/karnel/tools/games/detective/install.sh +51 -0
  71. package/karnel/tools/games/pet-friends/__pycache__/pet-friends.cpython-314.pyc +0 -0
  72. package/karnel/tools/games/pet-friends/install.sh +51 -0
  73. package/karnel/tools/games/pet-friends/pet-friends.py +15802 -0
  74. package/karnel/tools/games/tamagotchi/__pycache__/tamagotchi.cpython-314.pyc +0 -0
  75. package/karnel/tools/games/tamagotchi/install.sh +51 -0
  76. package/karnel/tools/games/tamagotchi/tamagotchi.py +1224 -0
  77. package/karnel/tools/games/terminal-arcade/__pycache__/terminal-arcade.cpython-314.pyc +0 -0
  78. package/karnel/tools/games/terminal-arcade/terminal-arcade.py +193 -0
  79. package/karnel/tools/npm/all.sh +1 -0
  80. package/karnel/utils/log.sh +34 -12
  81. package/package.json +1 -1
@@ -0,0 +1,749 @@
1
+ #!/data/data/com.termux/files/usr/bin/python3
2
+
3
+ import sys
4
+ import os
5
+ import subprocess
6
+ import importlib.util
7
+ import time
8
+ import curses
9
+ import traceback
10
+ import zipfile
11
+ import tarfile
12
+ import csv
13
+ import json
14
+ import gzip
15
+ import shutil
16
+ from contextlib import redirect_stderr, redirect_stdout
17
+
18
+ # --- 1. SETUP & CONFIGURATION ---
19
+
20
+ # System packages required for Termux (Binaries + Build tools for Python libs)
21
+ TERMUX_PACKAGES = [
22
+ "ffmpeg",
23
+ "unrar",
24
+ "libcairo",
25
+ "libgirepository",
26
+ "libjpeg-turbo",
27
+ "libpng",
28
+ "python",
29
+ "clang", # Required to build some python libs
30
+ "make", # Required to build some python libs
31
+ "binutils", # Required to build some python libs
32
+ "libffi"
33
+ ]
34
+
35
+ # (14) Python libraries to auto-install
36
+ REQUIRED_MODULES = {
37
+ "Pillow": "Pillow", # Images
38
+ "reportlab": "reportlab", # PDF creation
39
+ "docx": "python-docx", # Word docs
40
+ "odf": "odfpy", # OpenOffice docs
41
+ "bs4": "beautifulsoup4", # HTML/XML parsing
42
+ "markdown": "Markdown", # Markdown parsing
43
+ "lxml": "lxml", # XML/HTML parsing
44
+ "cairosvg": "cairosvg", # SVG conversion
45
+ "psd_tools": "psd-tools", # PSD reading
46
+ "striprtf": "striprtf", # RTF reading
47
+ "EbookLib": "EbookLib", # EPUB reading
48
+ "pptx": "python-pptx", # PowerPoint reading
49
+ "rarfile": "rarfile", # RAR extraction
50
+ "py7zr": "py7zr" # 7-Zip extraction
51
+ }
52
+
53
+ # (40) Folders to create
54
+ FOLDER_NAMES = [
55
+ "JPG", "PNG", "WEBP", "BMP", "TIFF", "GIF", "ICO", "TGA", "SVG", "PSD",
56
+ "PDF", "TXT", "DOCX", "ODT", "HTML", "MD", "CSV", "RTF", "EPUB", "JSON", "XML", "PPTX",
57
+ "ZIP", "TAR", "RAR", "7Z", "GZ",
58
+ "MP3", "WAV", "OGG", "FLAC", "M4A", "AAC", "WMA",
59
+ "MP4", "MKV", "AVI", "MOV", "WMV", "FLV"
60
+ ]
61
+
62
+ IMAGE_FOLDERS = ["JPG", "PNG", "WEBP", "BMP", "TIFF", "GIF", "ICO", "TGA", "SVG", "PSD"]
63
+ IMAGE_EXTS = ['.jpg', '.jpeg', '.png', '.webp', '.bmp', '.tiff', '.gif', '.ico', '.tga']
64
+ VECTOR_IMAGE_EXTS = ['.svg']
65
+ LAYERED_IMAGE_EXTS = ['.psd']
66
+ AV_EXTS = ['.mp3', '.wav', '.ogg', '.flac', '.m4a', '.aac', '.wma', '.mp4', '.mkv', '.avi', '.mov', '.wmv', '.flv']
67
+ ARCHIVE_EXTS = ['.zip', '.tar', '.gz', '.bz2', '.rar', '.7z']
68
+ TEXT_DOC_EXTS = ['.txt', '.docx', '.odt', '.html', '.md', '.csv', '.rtf', '.epub', '.json', '.xml', '.pptx', '.svg']
69
+
70
+ STORAGE_PATH = "/storage/emulated/0"
71
+ DOWNLOAD_PATH = os.path.join(STORAGE_PATH, "Download")
72
+ BASE_CONVERTER_PATH = os.path.join(DOWNLOAD_PATH, "File Converter")
73
+
74
+ HAS_FFMPEG = False
75
+ HAS_UNRAR = False
76
+ HAS_CAIRO = False
77
+
78
+ # --- 2. AUTO-INSTALLATION FUNCTIONS ---
79
+
80
+ def clear_screen_standard():
81
+ os.system('clear')
82
+
83
+ def install_termux_system_deps():
84
+ """Automatically installs required system packages via pkg."""
85
+ print("--- 1/4 Checking System Dependencies (Termux) ---")
86
+
87
+ # Check if we are likely in Termux
88
+ if not os.path.exists("/data/data/com.termux/files/usr/bin/pkg"):
89
+ print("Not running in standard Termux environment. Skipping system pkg install.")
90
+ return
91
+
92
+ print("Updating package lists and installing binaries...")
93
+ print("This may take a minute. Please wait...")
94
+
95
+ try:
96
+ # Update repositories
97
+ subprocess.run(["pkg", "update", "-y"], check=False)
98
+
99
+ # Install packages
100
+ cmd = ["pkg", "install", "-y"] + TERMUX_PACKAGES
101
+ subprocess.run(cmd, check=True)
102
+ print("System binaries installed successfully.\n")
103
+ except Exception as e:
104
+ print(f"WARNING: System install failed: {e}")
105
+ print("Attempting to continue, but some features (FFmpeg, Cairo) may fail.\n")
106
+ time.sleep(1)
107
+
108
+ def check_and_install_python_deps():
109
+ """Checks and installs required Python modules."""
110
+ print("--- 2/4 Checking Python Libraries ---")
111
+
112
+ # Upgrade pip first to ensure we can install wheels correctly
113
+ try:
114
+ subprocess.run([sys.executable, "-m", "pip", "install", "--upgrade", "pip"],
115
+ stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
116
+ except:
117
+ pass
118
+
119
+ all_installed = True
120
+ for module_name, package_name in REQUIRED_MODULES.items():
121
+ spec = importlib.util.find_spec(module_name)
122
+ if spec is None:
123
+ all_installed = False
124
+ print(f"Installing '{package_name}'... (Building wheels may take time)")
125
+ try:
126
+ # We use subprocess directly to see output if it fails,
127
+ # but hide it if it succeeds to keep UI clean.
128
+ subprocess.run([sys.executable, "-m", "pip", "install", package_name], check=True)
129
+ print(f"✔ Installed '{package_name}'.")
130
+ except Exception:
131
+ print(f"✖ ERROR: Failed to install '{package_name}'.")
132
+ print(f" Try manually: pip install {package_name}")
133
+ # We don't exit here, we try to keep going
134
+ else:
135
+ pass
136
+
137
+ if all_installed:
138
+ print("All Python libraries are present.\n")
139
+ else:
140
+ print("Library check complete.\n")
141
+ time.sleep(0.5)
142
+
143
+ def check_external_bins_status():
144
+ """Checks which external binaries are actually available after install."""
145
+ global HAS_FFMPEG, HAS_UNRAR, HAS_CAIRO
146
+ print("--- 3/4 Verifying Integrations ---")
147
+
148
+ # Check ffmpeg
149
+ if shutil.which("ffmpeg"):
150
+ print("✔ 'ffmpeg' detected. A/V conversions ENABLED.")
151
+ HAS_FFMPEG = True
152
+ else:
153
+ print("✖ 'ffmpeg' NOT found. A/V conversions DISABLED.")
154
+ HAS_FFMPEG = False
155
+
156
+ # Check unrar
157
+ if shutil.which("unrar"):
158
+ print("✔ 'unrar' detected. RAR extraction ENABLED.")
159
+ HAS_UNRAR = True
160
+ else:
161
+ print("✖ 'unrar' NOT found. RAR extraction DISABLED.")
162
+ HAS_UNRAR = False
163
+
164
+ # Check cairo
165
+ if importlib.util.find_spec("cairosvg") is not None:
166
+ # Basic check if the library loads without DLL errors
167
+ try:
168
+ import cairosvg
169
+ HAS_CAIRO = True
170
+ print("✔ 'cairosvg' loaded. SVG conversions ENABLED.")
171
+ except OSError:
172
+ print("✖ 'cairosvg' installed but system libs missing. SVG DISABLED.")
173
+ HAS_CAIRO = False
174
+ else:
175
+ print("✖ 'cairosvg' not found. SVG DISABLED.")
176
+ HAS_CAIRO = False
177
+
178
+ print("")
179
+ time.sleep(0.5)
180
+
181
+ def ensure_storage_access():
182
+ print("--- 4/4 Checking Storage Access ---")
183
+ if not os.path.exists(DOWNLOAD_PATH):
184
+ print(f"Access to '{DOWNLOAD_PATH}' denied or missing.")
185
+ print("Attempting to request storage permissions...")
186
+ try:
187
+ subprocess.run(["termux-setup-storage"], check=True)
188
+ print("Permission requested. Please allow it in the popup.")
189
+ print("Waiting 5 seconds for permission to propagate...")
190
+ time.sleep(5)
191
+ except FileNotFoundError:
192
+ print("Could not run 'termux-setup-storage'.")
193
+
194
+ if not os.path.exists(DOWNLOAD_PATH):
195
+ print(f"ERROR: Still cannot access '{DOWNLOAD_PATH}'.")
196
+ print("Please restart Termux and run this script again.")
197
+ sys.exit(1)
198
+
199
+ print("Storage access confirmed.\n")
200
+ time.sleep(0.5)
201
+
202
+ def setup_folders():
203
+ try:
204
+ os.makedirs(BASE_CONVERTER_PATH, exist_ok=True)
205
+ for folder in FOLDER_NAMES:
206
+ os.makedirs(os.path.join(BASE_CONVERTER_PATH, folder), exist_ok=True)
207
+ except Exception as e:
208
+ print(f"ERROR: Could not create folders: {e}")
209
+ sys.exit(1)
210
+
211
+ # --- 3. IMPORTS (Post-Installation) ---
212
+ # These are imported inside functions or wrapped to prevent crash before install finishes
213
+
214
+ # --- 4. CORE CONVERSION LOGIC ---
215
+
216
+ def get_text_from_file(input_path, in_ext):
217
+ text_lines = []
218
+ try:
219
+ if in_ext == '.txt':
220
+ with open(input_path, 'r', encoding='utf-8') as f:
221
+ text_lines = f.readlines()
222
+ elif in_ext == '.docx':
223
+ from docx import Document
224
+ doc = Document(input_path)
225
+ text_lines = [para.text + '\n' for para in doc.paragraphs]
226
+ elif in_ext == '.odt':
227
+ from odf.opendocument import load as odf_load
228
+ from odf.text import P as odf_P
229
+ doc = odf_load(input_path)
230
+ for para in doc.getElementsByType(odf_P):
231
+ text_lines.append(str(para) + '\n')
232
+ elif in_ext in ['.html', '.xml', '.svg']:
233
+ from bs4 import BeautifulSoup
234
+ with open(input_path, 'r', encoding='utf-8') as f:
235
+ parser = 'lxml' if in_ext != '.html' else 'html.parser'
236
+ soup = BeautifulSoup(f, parser)
237
+ text_lines = [line + '\n' for line in soup.get_text().splitlines()]
238
+ elif in_ext == '.md':
239
+ import markdown
240
+ from bs4 import BeautifulSoup
241
+ with open(input_path, 'r', encoding='utf-8') as f:
242
+ html = markdown.markdown(f.read())
243
+ soup = BeautifulSoup(html, 'html.parser')
244
+ text_lines = [line + '\n' for line in soup.get_text().splitlines()]
245
+ elif in_ext == '.csv':
246
+ with open(input_path, 'r', encoding='utf-8', newline='') as f:
247
+ reader = csv.reader(f)
248
+ text_lines = [','.join(row) + '\n' for row in reader]
249
+ elif in_ext == '.json':
250
+ with open(input_path, 'r', encoding='utf-8') as f:
251
+ data = json.load(f)
252
+ text_lines = [json.dumps(data, indent=2)]
253
+ elif in_ext == '.rtf':
254
+ from striprtf.striprtf import rtf_to_text
255
+ with open(input_path, 'r', encoding='utf-8') as f:
256
+ content = f.read()
257
+ text_lines = [rtf_to_text(content)]
258
+ elif in_ext == '.epub':
259
+ from ebooklib import epub, ITEM_DOCUMENT
260
+ from bs4 import BeautifulSoup
261
+ book = epub.read_epub(input_path)
262
+ for item in book.get_items():
263
+ if item.get_type() == ITEM_DOCUMENT:
264
+ soup = BeautifulSoup(item.get_content(), 'html.parser')
265
+ text_lines.append(soup.get_text() + '\n\n')
266
+ elif in_ext == '.pptx':
267
+ import pptx
268
+ prs = pptx.Presentation(input_path)
269
+ for slide in prs.slides:
270
+ for shape in slide.shapes:
271
+ if shape.has_text_frame:
272
+ text_lines.append(shape.text + '\n')
273
+ except Exception as e:
274
+ raise Exception(f"Text extraction failed: {e}")
275
+ return text_lines
276
+
277
+ def write_text_to_pdf(text_lines, output_path):
278
+ from reportlab.pdfgen import canvas
279
+ from reportlab.lib.pagesizes import A4
280
+ from reportlab.lib.units import inch
281
+
282
+ c = canvas.Canvas(output_path, pagesize=A4)
283
+ width, height = A4
284
+ margin_x, margin_y = 0.75 * inch, 1 * inch
285
+ text_object = c.beginText(margin_x, height - margin_y)
286
+ text_object.setFont("Helvetica", 10)
287
+ line_height, y = 12, height - margin_y
288
+ for line in text_lines:
289
+ for sub_line in line.split('\n'):
290
+ if y < margin_y:
291
+ c.drawText(text_object)
292
+ c.showPage()
293
+ text_object = c.beginText(margin_x, height - margin_y)
294
+ text_object.setFont("Helvetica", 10)
295
+ y = height - margin_y
296
+ text_object.textLine(sub_line.strip('\r'))
297
+ y -= line_height
298
+ c.drawText(text_object)
299
+ c.save()
300
+
301
+ def handle_image_conversion(stdscr, in_path, out_path):
302
+ from PIL import Image
303
+ with Image.open(in_path) as img:
304
+ if out_path.lower().endswith(('.jpg', '.jpeg')):
305
+ if img.mode == 'RGBA':
306
+ img = img.convert('RGB')
307
+ img.save(out_path)
308
+
309
+ def handle_svg_conversion(stdscr, in_path, out_path):
310
+ if not HAS_CAIRO:
311
+ raise Exception("Cairo/SVG libraries not installed/loaded.")
312
+ import cairosvg
313
+ out_ext = os.path.splitext(out_path)[1].lower()
314
+ if out_ext == '.png':
315
+ cairosvg.svg2png(url=in_path, write_to=out_path)
316
+ elif out_ext == '.pdf':
317
+ cairosvg.svg2pdf(url=in_path, write_to=out_path)
318
+ else:
319
+ raise Exception(f"SVG conversion to {out_ext} not supported.")
320
+
321
+ def handle_psd_conversion(stdscr, in_path, out_path):
322
+ from psd_tools import PSDImage
323
+ psd = PSDImage.open(in_path)
324
+ composite_image = psd.composite()
325
+ composite_image.save(out_path)
326
+
327
+ def handle_av_conversion(stdscr, in_path, out_path):
328
+ if not HAS_FFMPEG:
329
+ raise Exception("'ffmpeg' not found. A/V conversion is disabled.")
330
+ command = ['ffmpeg', '-i', in_path, '-y', out_path]
331
+ curses.endwin()
332
+ print("--- Running ffmpeg ---")
333
+ print(f"Command: {' '.join(command)}")
334
+ try:
335
+ # Using subprocess directly to show output
336
+ subprocess.run(command, check=True)
337
+ print("ffmpeg conversion finished successfully.")
338
+ except Exception as e:
339
+ print(f"ffmpeg ERROR: {e}")
340
+ raise Exception(f"ffmpeg conversion failed. {e}")
341
+ finally:
342
+ print("Press Enter to return to the app...")
343
+ sys.stdin.read(1)
344
+ stdscr.refresh()
345
+
346
+ def handle_extraction(stdscr, in_path, out_folder_path, in_ext):
347
+ base_name = os.path.splitext(os.path.basename(in_path))[0]
348
+ extract_path = os.path.join(out_folder_path, base_name)
349
+ os.makedirs(extract_path, exist_ok=True)
350
+
351
+ if in_ext == '.zip':
352
+ with zipfile.ZipFile(in_path, 'r') as zf:
353
+ zf.extractall(extract_path)
354
+ elif in_ext in ['.tar', '.gz', '.bz2']:
355
+ if in_ext == '.gz' and not in_path.endswith('.tar.gz'):
356
+ out_filename = os.path.splitext(os.path.basename(in_path))[0]
357
+ out_path = os.path.join(out_folder_path, out_filename)
358
+ with gzip.open(in_path, 'rb') as f_in:
359
+ with open(out_path, 'wb') as f_out:
360
+ shutil.copyfileobj(f_in, f_out)
361
+ return f"Decompressed to: {out_path}"
362
+ else:
363
+ with tarfile.open(in_path, 'r:*') as tf:
364
+ tf.extractall(extract_path)
365
+ elif in_ext == '.rar':
366
+ if not HAS_UNRAR:
367
+ raise Exception("'unrar' binary not found.")
368
+ import rarfile
369
+ with rarfile.RarFile(in_path) as rf:
370
+ rf.extractall(extract_path)
371
+ elif in_ext == '.7z':
372
+ import py7zr
373
+ with py7zr.SevenZipFile(in_path, 'r') as zf:
374
+ zf.extractall(extract_path)
375
+
376
+ return f"Extracted to: {extract_path}"
377
+
378
+ def handle_data_conversion(stdscr, in_path, out_path, in_ext, out_ext):
379
+ if in_ext == '.csv' and out_ext == '.json':
380
+ data = []
381
+ with open(in_path, 'r', encoding='utf-8', newline='') as f:
382
+ reader = csv.DictReader(f)
383
+ for row in reader:
384
+ data.append(row)
385
+ with open(out_path, 'w', encoding='utf-8') as f:
386
+ json.dump(data, f, indent=4)
387
+ elif in_ext == '.json' and out_ext == '.csv':
388
+ with open(in_path, 'r', encoding='utf-8') as f:
389
+ data = json.load(f)
390
+ if not isinstance(data, list) or not data:
391
+ raise Exception("JSON must be a non-empty list of objects.")
392
+ headers = data[0].keys()
393
+ with open(out_path, 'w', encoding='utf-8', newline='') as f:
394
+ writer = csv.DictWriter(f, fieldnames=headers)
395
+ writer.writeheader()
396
+ writer.writerows(data)
397
+ else:
398
+ raise Exception(f"Data conversion {in_ext} to {out_ext} not supported.")
399
+
400
+ def handle_md_to_html(stdscr, in_path, out_path):
401
+ import markdown
402
+ with open(in_path, 'r', encoding='utf-8') as f:
403
+ html = markdown.markdown(f.read())
404
+ with open(out_path, 'w', encoding='utf-8') as f:
405
+ f.write(html)
406
+
407
+ def handle_single_image_to_pdf(stdscr, in_path, out_path):
408
+ from PIL import Image
409
+ try:
410
+ with Image.open(in_path) as img:
411
+ img_rgb = img.convert('RGB')
412
+ img_rgb.save(out_path, "PDF", resolution=100.0)
413
+ except Exception as e:
414
+ raise Exception(f"Pillow (Image->PDF) Error: {e}")
415
+
416
+ def handle_multi_image_to_pdf(stdscr, image_paths, out_path):
417
+ from PIL import Image
418
+ try:
419
+ images_rgb = []
420
+ for path in image_paths:
421
+ img = Image.open(path)
422
+ images_rgb.append(img.convert('RGB'))
423
+ if not images_rgb:
424
+ raise Exception("No images found to convert.")
425
+ images_rgb[0].save(
426
+ out_path, "PDF", resolution=100.0,
427
+ save_all=True, append_images=images_rgb[1:]
428
+ )
429
+ except Exception as e:
430
+ raise Exception(f"Pillow (Multi-Image->PDF) Error: {e}")
431
+
432
+ # --- 5. MAIN CONVERSION ROUTER ---
433
+
434
+ def convert_file(stdscr, in_path, out_folder_name):
435
+ in_ext = os.path.splitext(in_path)[1].lower()
436
+ out_ext = f".{out_folder_name.lower()}"
437
+ base_name = os.path.splitext(os.path.basename(in_path))[0]
438
+ out_folder_path = os.path.join(BASE_CONVERTER_PATH, out_folder_name)
439
+ out_path = os.path.join(out_folder_path, f"{base_name}{out_ext}")
440
+
441
+ try:
442
+ if in_ext in ARCHIVE_EXTS:
443
+ out_folder = out_folder_path if in_ext == '.gz' else os.path.join(BASE_CONVERTER_PATH, out_folder_name)
444
+ message = handle_extraction(stdscr, in_path, out_folder, in_ext)
445
+ return (True, message)
446
+
447
+ if in_ext in VECTOR_IMAGE_EXTS and out_ext in ['.png', '.pdf']:
448
+ handle_svg_conversion(stdscr, in_path, out_path)
449
+ return (True, f"Saved to: {out_path}")
450
+
451
+ if in_ext in LAYERED_IMAGE_EXTS and out_ext in IMAGE_EXTS:
452
+ handle_psd_conversion(stdscr, in_path, out_path)
453
+ return (True, f"Saved to: {out_path}")
454
+
455
+ if in_ext in IMAGE_EXTS and out_ext in IMAGE_EXTS:
456
+ handle_image_conversion(stdscr, in_path, out_path)
457
+ return (True, f"Saved to: {out_path}")
458
+
459
+ if in_ext in IMAGE_EXTS and out_ext == '.pdf':
460
+ handle_single_image_to_pdf(stdscr, in_path, out_path)
461
+ return (True, f"Saved to: {out_path}")
462
+
463
+ if in_ext in AV_EXTS and out_ext in AV_EXTS:
464
+ handle_av_conversion(stdscr, in_path, out_path)
465
+ return (True, f"Saved to: {out_path}")
466
+
467
+ if in_ext in ['.csv', '.json'] and out_ext in ['.csv', '.json']:
468
+ handle_data_conversion(stdscr, in_path, out_path, in_ext, out_ext)
469
+ return (True, f"Saved to: {out_path}")
470
+
471
+ if in_ext == '.md' and out_ext == '.html':
472
+ handle_md_to_html(stdscr, in_path, out_path)
473
+ return (True, f"Saved to: {out_path}")
474
+
475
+ if out_ext == '.txt' and in_ext in TEXT_DOC_EXTS:
476
+ text_lines = get_text_from_file(in_path, in_ext)
477
+ with open(out_path, 'w', encoding='utf-8') as f:
478
+ f.writelines(text_lines)
479
+ return (True, f"Saved to: {out_path}")
480
+
481
+ if out_ext == '.pdf' and in_ext in TEXT_DOC_EXTS:
482
+ text_lines = get_text_from_file(in_path, in_ext)
483
+ write_text_to_pdf(text_lines, out_path)
484
+ return (True, f"Saved to: {out_path}")
485
+
486
+ return (False, f"Unsupported conversion: {in_ext} to {out_ext}")
487
+
488
+ except Exception as e:
489
+ return (False, f"ERROR: {str(e)}")
490
+
491
+
492
+ # --- 6. CURSES UI HELPER FUNCTIONS ---
493
+
494
+ def draw_header(stdscr, title):
495
+ h, w = stdscr.getmaxyx()
496
+ stdscr.attron(curses.color_pair(2))
497
+ stdscr.addstr(0, 0, " " * w, curses.color_pair(2))
498
+ header_text = f" Termux Curses Converter (q to quit/back) "
499
+ stdscr.addstr(0, (w - len(header_text)) // 2, header_text, curses.A_REVERSE)
500
+ stdscr.attroff(curses.color_pair(2))
501
+ stdscr.attron(curses.color_pair(1))
502
+ stdscr.addstr(2, (w - len(title)) // 2, title)
503
+ stdscr.attroff(curses.color_pair(1))
504
+
505
+ def draw_status(stdscr, message, is_error=False):
506
+ h, w = stdscr.getmaxyx()
507
+ nav_hint = " (Arrows/Enter to Select, q to Back) "
508
+ hint_len = len(nav_hint)
509
+ color = curses.color_pair(3) if is_error else curses.color_pair(2)
510
+ stdscr.attron(color)
511
+ stdscr.addstr(h - 1, 0, " " * (w - 1))
512
+ stdscr.addstr(h - 1, 1, message[:w - hint_len - 2])
513
+ stdscr.attron(curses.A_REVERSE)
514
+ stdscr.addstr(h - 1, w - hint_len - 1, nav_hint)
515
+ stdscr.attroff(curses.A_REVERSE)
516
+ stdscr.attroff(color)
517
+
518
+ def run_menu(stdscr, title, options, sub_title=""):
519
+ current_idx = 0
520
+ while True:
521
+ stdscr.clear()
522
+ h, w = stdscr.getmaxyx()
523
+ draw_header(stdscr, title)
524
+ if sub_title:
525
+ stdscr.addstr(4, (w - len(sub_title)) // 2, sub_title)
526
+ draw_status(stdscr, "Select an option.")
527
+
528
+ for i, option in enumerate(options):
529
+ display_option = option
530
+ if len(option) > w - 8:
531
+ display_option = option[:w - 11] + "..."
532
+ x = (w - len(display_option)) // 2 - 2
533
+ y = h // 2 - len(options) // 2 + i
534
+ if i == current_idx:
535
+ stdscr.attron(curses.color_pair(1))
536
+ stdscr.addstr(y, x, f"> {display_option} <")
537
+ stdscr.attroff(curses.color_pair(1))
538
+ else:
539
+ stdscr.addstr(y, x, f" {display_option} ")
540
+
541
+ stdscr.refresh()
542
+ key = stdscr.getch()
543
+ if key == curses.KEY_UP:
544
+ current_idx = (current_idx - 1) % len(options)
545
+ elif key == curses.KEY_DOWN:
546
+ current_idx = (current_idx + 1) % len(options)
547
+ elif key in [curses.KEY_ENTER, 10, 13]:
548
+ return options[current_idx]
549
+ elif key == ord('q'):
550
+ return None
551
+
552
+ def run_file_selector(stdscr, folder_path, title, input_folder_name):
553
+ try:
554
+ files = [f for f in os.listdir(folder_path) if os.path.isfile(os.path.join(folder_path, f))]
555
+ files.sort()
556
+ except Exception as e:
557
+ draw_status(stdscr, f"Error reading {folder_path}: {e}", is_error=True)
558
+ stdscr.getch()
559
+ return None
560
+
561
+ if not files:
562
+ draw_status(stdscr, f"No files found in {os.path.basename(folder_path)}", is_error=True)
563
+ stdscr.getch()
564
+ return None
565
+
566
+ options = ["[ .. Go Back .. ]"]
567
+ if input_folder_name in IMAGE_FOLDERS:
568
+ options.append(f"[ ** Convert ALL {len(files)} Images in '{input_folder_name}' to one PDF ** ]")
569
+ options.extend(files)
570
+ selection = run_menu(stdscr, title, options, f"Folder: /Download/File Converter/{input_folder_name}")
571
+ if selection == "[ .. Go Back .. ]":
572
+ return None
573
+ return selection
574
+
575
+ def run_confirmation(stdscr, prompt):
576
+ options = ["Yes", "No"]
577
+ selection = run_menu(stdscr, prompt, options, "Please confirm")
578
+ return selection
579
+
580
+ def run_help(stdscr):
581
+ stdscr.clear()
582
+ h, w = stdscr.getmaxyx()
583
+ draw_header(stdscr, "How to Use")
584
+ help_text = [
585
+ "1. MOVE FILES:",
586
+ f" Go to: /Download/File Converter/",
587
+ " Move files into the correct INPUT folder.",
588
+ "",
589
+ "2. RUN CONVERTER:",
590
+ " Select 'Convert a File' -> INPUT folder -> File.",
591
+ "",
592
+ "3. SELECT OUTPUT:",
593
+ " Select the format you want.",
594
+ "",
595
+ "** NOTES **",
596
+ " - Archives extract to their own folder.",
597
+ " - A/V conversion uses FFmpeg (Auto-installed).",
598
+ " - Images can be combined into one PDF."
599
+ ]
600
+ for i, line in enumerate(help_text):
601
+ if 5 + i >= h - 2: break
602
+ stdscr.addstr(5 + i, (w - len(line)) // 2, line)
603
+ draw_status(stdscr, "Press 'q' or Enter to go back.")
604
+ stdscr.refresh()
605
+ while True:
606
+ key = stdscr.getch()
607
+ if key in [ord('q'), curses.KEY_ENTER, 10, 13]:
608
+ return
609
+
610
+ def run_text_input(stdscr, prompt):
611
+ stdscr.clear()
612
+ h, w = stdscr.getmaxyx()
613
+ draw_header(stdscr, "Input Required")
614
+ stdscr.addstr(h // 2 - 1, (w - len(prompt)) // 2, prompt)
615
+ box_y, box_x = h // 2 + 1, w // 2 - 20
616
+ stdscr.attron(curses.color_pair(2))
617
+ stdscr.addstr(box_y, box_x, " " * 40)
618
+ stdscr.attroff(curses.color_pair(2))
619
+ draw_status(stdscr, "Type filename. Enter to Confirm.")
620
+ curses.curs_set(1)
621
+ stdscr.keypad(True)
622
+ text = ""
623
+ while True:
624
+ stdscr.attron(curses.color_pair(2))
625
+ stdscr.addstr(box_y, box_x, " " * 40)
626
+ stdscr.addstr(box_y, box_x + 1, text[:38])
627
+ stdscr.attroff(curses.color_pair(2))
628
+ stdscr.move(box_y, box_x + 1 + len(text))
629
+ stdscr.refresh()
630
+ key = stdscr.getch()
631
+ if key in [curses.KEY_ENTER, 10, 13]:
632
+ break
633
+ elif key == ord('q'):
634
+ text = None; break
635
+ elif key in [curses.KEY_BACKSPACE, 127, 8]:
636
+ text = text[:-1]
637
+ elif 32 <= key <= 126:
638
+ if len(text) < 38: text += chr(key)
639
+ curses.curs_set(0); stdscr.keypad(False); return text
640
+
641
+ def run_multi_image_to_pdf_wizard(stdscr, input_folder_path, input_folder_name):
642
+ try:
643
+ image_paths = [
644
+ os.path.join(input_folder_path, f)
645
+ for f in os.listdir(input_folder_path)
646
+ if os.path.splitext(f)[1].lower() in IMAGE_EXTS
647
+ ]
648
+ image_paths.sort()
649
+ except Exception as e:
650
+ draw_status(stdscr, f"Error: {e}", is_error=True); stdscr.getch(); return
651
+ if not image_paths:
652
+ draw_status(stdscr, "No images found.", is_error=True); stdscr.getch(); return
653
+ confirm = run_confirmation(stdscr, f"Combine {len(image_paths)} images into one PDF?")
654
+ if confirm != "Yes": return
655
+ default_name = f"{input_folder_name}_Album"
656
+ filename = run_text_input(stdscr, f"Enter PDF name (default: {default_name})")
657
+ if filename is None: return
658
+ if not filename: filename = default_name
659
+ out_folder_path = os.path.join(BASE_CONVERTER_PATH, "PDF")
660
+ out_path = os.path.join(out_folder_path, f"{filename}.pdf")
661
+ draw_status(stdscr, "Working..."); stdscr.refresh()
662
+ try:
663
+ handle_multi_image_to_pdf(stdscr, image_paths, out_path)
664
+ draw_status(stdscr, f"Success! Saved to: /PDF/{filename}.pdf")
665
+ except Exception as e:
666
+ draw_status(stdscr, f"ERROR: {e}", is_error=True)
667
+ stdscr.getch()
668
+
669
+ # --- 7. MAIN CURSES APPLICATION ---
670
+
671
+ def main(stdscr):
672
+ curses.curs_set(0); stdscr.nodelay(0); stdscr.timeout(-1)
673
+ curses.start_color()
674
+ curses.init_pair(1, curses.COLOR_CYAN, curses.COLOR_BLACK)
675
+ curses.init_pair(2, curses.COLOR_BLACK, curses.COLOR_WHITE)
676
+ curses.init_pair(3, curses.COLOR_WHITE, curses.COLOR_RED)
677
+
678
+ while True:
679
+ main_choice = run_menu(stdscr, "Main Menu", ["Convert a File", "Help / How to Use", "Exit"])
680
+ if main_choice == "Exit" or main_choice is None: break
681
+ if main_choice == "Help / How to Use": run_help(stdscr); continue
682
+
683
+ input_folder = run_menu(stdscr, "Step 1: Choose INPUT Folder", FOLDER_NAMES)
684
+ if input_folder is None: continue
685
+ input_folder_path = os.path.join(BASE_CONVERTER_PATH, input_folder)
686
+ input_file = run_file_selector(stdscr, input_folder_path, f"Step 2: Choose File from '{input_folder}'", input_folder)
687
+ if input_file is None: continue
688
+
689
+ if input_file.startswith("[ ** Convert ALL"):
690
+ run_multi_image_to_pdf_wizard(stdscr, input_folder_path, input_folder)
691
+ continue
692
+
693
+ full_input_path = os.path.join(input_folder_path, input_file)
694
+ in_ext = os.path.splitext(input_file)[1].lower()
695
+ if in_ext in ARCHIVE_EXTS:
696
+ output_folder = input_folder
697
+ prompt = f"Extract '{input_file}' here?"
698
+ else:
699
+ output_folder = run_menu(stdscr, "Step 3: Choose OUTPUT Format", FOLDER_NAMES)
700
+ if output_folder is None: continue
701
+ if output_folder == input_folder:
702
+ draw_status(stdscr, "Input and Output cannot be the same.", is_error=True)
703
+ stdscr.getch(); continue
704
+ prompt = f"Convert '{input_file}' to {output_folder}?"
705
+
706
+ confirm = run_confirmation(stdscr, prompt)
707
+ if confirm != "Yes": continue
708
+
709
+ draw_status(stdscr, "Working, please wait..."); stdscr.refresh()
710
+ success, message = convert_file(stdscr, full_input_path, output_folder)
711
+ draw_status(stdscr, message, is_error=not success)
712
+ stdscr.getch()
713
+
714
+ # --- 8. SCRIPT ENTRYPOINT ---
715
+
716
+ if __name__ == "__main__":
717
+ try:
718
+ clear_screen_standard()
719
+ print("--- Initializing Termux Converter v3.2 (Auto-Install) ---")
720
+
721
+ # 1. Install System Binaries (pkg)
722
+ install_termux_system_deps()
723
+
724
+ # 2. Install Python Modules (pip)
725
+ check_and_install_python_deps()
726
+
727
+ # 3. Verify Integrations
728
+ check_external_bins_status()
729
+
730
+ # 4. Check Storage
731
+ ensure_storage_access()
732
+
733
+ setup_folders()
734
+
735
+ print("--- Setup Complete ---")
736
+ print(f"Folders ready in: /storage/emulated/0/Download/File Converter/")
737
+ print("\nStarting application...")
738
+ time.sleep(1)
739
+
740
+ curses.wrapper(main)
741
+ print("File Converter exited successfully.")
742
+
743
+ except KeyboardInterrupt:
744
+ print("\nExiting...")
745
+ except Exception as e:
746
+ print("\nA critical error occurred:")
747
+ traceback.print_exc()
748
+ finally:
749
+ os.system('clear')