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.
- package/README.md +230 -225
- package/install.sh +30 -9
- package/karnel/bin/karnel +2 -2
- package/karnel/cli/commands/backup.sh +10 -2
- package/karnel/cli/commands/brain.sh +28 -23
- package/karnel/cli/commands/cleanup.sh +10 -4
- package/karnel/cli/commands/deploy.sh +53 -0
- package/karnel/cli/commands/doctor/code.sh +225 -0
- package/karnel/cli/commands/doctor/code_detect.sh +184 -0
- package/karnel/cli/commands/doctor/code_exec.sh +278 -0
- package/karnel/cli/commands/doctor/code_langs.sh +229 -0
- package/karnel/cli/commands/doctor/fixes.sh +63 -46
- package/karnel/cli/commands/doctor.sh +222 -80
- package/karnel/cli/commands/env.sh +2 -0
- package/karnel/cli/commands/ia.sh +12 -2
- package/karnel/cli/commands/init.sh +17 -9
- package/karnel/cli/commands/install.sh +90 -54
- package/karnel/cli/commands/list.sh +36 -0
- package/karnel/cli/commands/pg.sh +21 -6
- package/karnel/cli/commands/reinstall.sh +99 -0
- package/karnel/cli/commands/restore.sh +43 -3
- package/karnel/cli/commands/search.sh +2 -2
- package/karnel/cli/commands/update.sh +6 -1
- package/karnel/cli/commands/upgrade.sh +8 -1
- package/karnel/cli/commands/voice.sh +2 -1
- package/karnel/cli/karnel.sh +11 -5
- package/karnel/modules/games.sh +11 -0
- package/karnel/tools/ai/all.sh +1 -0
- package/karnel/tools/ai/copilot-termux/README.md +40 -0
- package/karnel/tools/ai/copilot-termux/install.sh +80 -0
- package/karnel/tools/ai/copilot-termux/package.json +20 -0
- package/karnel/tools/db/all.sh +2 -0
- package/karnel/tools/deploy/railway/install.sh +2 -2
- package/karnel/tools/dev/all.sh +8 -0
- package/karnel/tools/dev/applaunch/__pycache__/applaunch.cpython-314.pyc +0 -0
- package/karnel/tools/dev/applaunch/applaunch.py +434 -0
- package/karnel/tools/dev/applaunch/install.sh +51 -0
- package/karnel/tools/dev/fconv/__pycache__/fconv.cpython-314.pyc +0 -0
- package/karnel/tools/dev/fconv/fconv.py +749 -0
- package/karnel/tools/dev/fconv/install.sh +51 -0
- package/karnel/tools/dev/filecheck/__pycache__/filecheck.cpython-314.pyc +0 -0
- package/karnel/tools/dev/filecheck/filecheck.py +429 -0
- package/karnel/tools/dev/filecheck/install.sh +51 -0
- package/karnel/tools/dev/notes/__pycache__/notes.cpython-314.pyc +0 -0
- package/karnel/tools/dev/notes/install.sh +51 -0
- package/karnel/tools/dev/notes/notes.py +849 -0
- package/karnel/tools/dev/passman/__pycache__/passman.cpython-314.pyc +0 -0
- package/karnel/tools/dev/passman/install.sh +51 -0
- package/karnel/tools/dev/passman/passman.py +536 -0
- package/karnel/tools/dev/splash/__pycache__/splash.cpython-314.pyc +0 -0
- package/karnel/tools/dev/splash/install.sh +51 -0
- package/karnel/tools/dev/splash/splash.py +188 -0
- package/karnel/tools/dev/treex/install.sh +51 -0
- package/karnel/tools/dev/treex/treex.py +2036 -0
- package/karnel/tools/dev/websites/__pycache__/websites.cpython-314.pyc +0 -0
- package/karnel/tools/dev/websites/install.sh +51 -0
- package/karnel/tools/dev/websites/websites.py +844 -0
- package/karnel/tools/games/all.sh +62 -0
- package/karnel/tools/games/arcade/__pycache__/arcade.cpython-314.pyc +0 -0
- package/karnel/tools/games/arcade/arcade.py +193 -0
- package/karnel/tools/games/arcade/install.sh +51 -0
- package/karnel/tools/games/buzz/__pycache__/buzz.cpython-314.pyc +0 -0
- package/karnel/tools/games/buzz/buzz.py +1264 -0
- package/karnel/tools/games/buzz/install.sh +51 -0
- package/karnel/tools/games/ctfgod/__pycache__/ctfgod.cpython-314.pyc +0 -0
- package/karnel/tools/games/ctfgod/ctfgod.py +1576 -0
- package/karnel/tools/games/ctfgod/install.sh +51 -0
- package/karnel/tools/games/detective/__pycache__/detective.cpython-314.pyc +0 -0
- package/karnel/tools/games/detective/detective.py +2091 -0
- package/karnel/tools/games/detective/install.sh +51 -0
- package/karnel/tools/games/pet-friends/__pycache__/pet-friends.cpython-314.pyc +0 -0
- package/karnel/tools/games/pet-friends/install.sh +51 -0
- package/karnel/tools/games/pet-friends/pet-friends.py +15802 -0
- package/karnel/tools/games/tamagotchi/__pycache__/tamagotchi.cpython-314.pyc +0 -0
- package/karnel/tools/games/tamagotchi/install.sh +51 -0
- package/karnel/tools/games/tamagotchi/tamagotchi.py +1224 -0
- package/karnel/tools/games/terminal-arcade/__pycache__/terminal-arcade.cpython-314.pyc +0 -0
- package/karnel/tools/games/terminal-arcade/terminal-arcade.py +193 -0
- package/karnel/tools/npm/all.sh +1 -0
- package/karnel/utils/log.sh +34 -12
- 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')
|