lattifai 0.4.4__py3-none-any.whl → 0.4.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- lattifai/__init__.py +26 -27
- lattifai/base_client.py +7 -7
- lattifai/bin/agent.py +94 -91
- lattifai/bin/align.py +110 -111
- lattifai/bin/cli_base.py +3 -3
- lattifai/bin/subtitle.py +45 -45
- lattifai/client.py +56 -56
- lattifai/errors.py +73 -73
- lattifai/io/__init__.py +12 -11
- lattifai/io/gemini_reader.py +30 -30
- lattifai/io/gemini_writer.py +17 -17
- lattifai/io/reader.py +13 -12
- lattifai/io/supervision.py +3 -3
- lattifai/io/text_parser.py +43 -16
- lattifai/io/utils.py +4 -4
- lattifai/io/writer.py +31 -19
- lattifai/tokenizer/__init__.py +1 -1
- lattifai/tokenizer/phonemizer.py +3 -3
- lattifai/tokenizer/tokenizer.py +84 -83
- lattifai/utils.py +15 -15
- lattifai/workers/__init__.py +1 -1
- lattifai/workers/lattice1_alpha.py +103 -63
- lattifai/workflows/__init__.py +11 -11
- lattifai/workflows/agents.py +2 -0
- lattifai/workflows/base.py +22 -22
- lattifai/workflows/file_manager.py +182 -182
- lattifai/workflows/gemini.py +29 -29
- lattifai/workflows/prompts/__init__.py +4 -4
- lattifai/workflows/youtube.py +233 -233
- {lattifai-0.4.4.dist-info → lattifai-0.4.6.dist-info}/METADATA +7 -10
- lattifai-0.4.6.dist-info/RECORD +39 -0
- {lattifai-0.4.4.dist-info → lattifai-0.4.6.dist-info}/licenses/LICENSE +1 -1
- lattifai-0.4.4.dist-info/RECORD +0 -39
- {lattifai-0.4.4.dist-info → lattifai-0.4.6.dist-info}/WHEEL +0 -0
- {lattifai-0.4.4.dist-info → lattifai-0.4.6.dist-info}/entry_points.txt +0 -0
- {lattifai-0.4.4.dist-info → lattifai-0.4.6.dist-info}/top_level.txt +0 -0
|
@@ -21,10 +21,10 @@ class FileExistenceManager:
|
|
|
21
21
|
"""Utility class for handling file existence checks and user confirmations"""
|
|
22
22
|
|
|
23
23
|
FILE_TYPE_INFO = {
|
|
24
|
-
|
|
24
|
+
"media": ("🎬", "Media"),
|
|
25
25
|
# 'audio': ('📱', 'Audio'),
|
|
26
26
|
# 'video': ('🎬', 'Video'),
|
|
27
|
-
|
|
27
|
+
"subtitle": ("📝", "Subtitle"),
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
@staticmethod
|
|
@@ -47,52 +47,52 @@ class FileExistenceManager:
|
|
|
47
47
|
Dictionary with 'media', 'subtitle' keys containing lists of existing files
|
|
48
48
|
"""
|
|
49
49
|
output_path = Path(output_dir).expanduser()
|
|
50
|
-
existing_files = {
|
|
50
|
+
existing_files = {"media": [], "subtitle": []}
|
|
51
51
|
|
|
52
52
|
if not output_path.exists():
|
|
53
53
|
return existing_files
|
|
54
54
|
|
|
55
55
|
# Default formats - combine audio and video formats
|
|
56
|
-
media_formats = media_formats or [
|
|
57
|
-
subtitle_formats = subtitle_formats or [
|
|
56
|
+
media_formats = media_formats or ["mp3", "wav", "m4a", "aac", "opus", "mp4", "webm", "mkv", "avi"]
|
|
57
|
+
subtitle_formats = subtitle_formats or ["md", "srt", "vtt", "ass", "ssa", "sub", "sbv", "txt"]
|
|
58
58
|
|
|
59
59
|
# Check for media files (audio and video)
|
|
60
60
|
for ext in set(media_formats): # Remove duplicates
|
|
61
61
|
# Pattern 1: Simple pattern like {video_id}.mp3
|
|
62
|
-
media_file = output_path / f
|
|
62
|
+
media_file = output_path / f"{video_id}.{ext}"
|
|
63
63
|
if media_file.exists():
|
|
64
|
-
existing_files[
|
|
64
|
+
existing_files["media"].append(str(media_file))
|
|
65
65
|
|
|
66
66
|
# Pattern 2: With suffix like {video_id}_Edit.mp3 or {video_id}.something.mp3
|
|
67
|
-
for media_file in output_path.glob(f
|
|
67
|
+
for media_file in output_path.glob(f"{video_id}*.{ext}"):
|
|
68
68
|
file_path = str(media_file)
|
|
69
|
-
if file_path not in existing_files[
|
|
70
|
-
existing_files[
|
|
69
|
+
if file_path not in existing_files["media"]:
|
|
70
|
+
existing_files["media"].append(file_path)
|
|
71
71
|
|
|
72
72
|
# Check for subtitle files
|
|
73
73
|
for ext in set(subtitle_formats): # Remove duplicates
|
|
74
74
|
# Check multiple naming patterns for subtitle files
|
|
75
75
|
# Pattern 1: Simple pattern like {video_id}.vtt
|
|
76
|
-
subtitle_file = output_path / f
|
|
76
|
+
subtitle_file = output_path / f"{video_id}.{ext}"
|
|
77
77
|
if subtitle_file.exists():
|
|
78
|
-
existing_files[
|
|
78
|
+
existing_files["subtitle"].append(str(subtitle_file))
|
|
79
79
|
|
|
80
80
|
# Pattern 2: With language/track suffix like {video_id}.en-trackid.vtt
|
|
81
|
-
for sub_file in output_path.glob(f
|
|
81
|
+
for sub_file in output_path.glob(f"{video_id}.*.{ext}"):
|
|
82
82
|
file_path = str(sub_file)
|
|
83
|
-
if file_path not in existing_files[
|
|
84
|
-
existing_files[
|
|
83
|
+
if file_path not in existing_files["subtitle"]:
|
|
84
|
+
existing_files["subtitle"].append(file_path)
|
|
85
85
|
|
|
86
|
-
if
|
|
86
|
+
if "md" in subtitle_formats:
|
|
87
87
|
# Gemini-specific pattern: {video_id}_Gemini.md
|
|
88
|
-
gemini_subtitle_file = output_path / f
|
|
88
|
+
gemini_subtitle_file = output_path / f"{video_id}_Gemini.md"
|
|
89
89
|
if gemini_subtitle_file.exists():
|
|
90
|
-
existing_files[
|
|
90
|
+
existing_files["subtitle"].append(str(gemini_subtitle_file))
|
|
91
91
|
|
|
92
92
|
return existing_files
|
|
93
93
|
|
|
94
94
|
@staticmethod
|
|
95
|
-
def prompt_user_confirmation(existing_files: Dict[str, List[str]], operation: str =
|
|
95
|
+
def prompt_user_confirmation(existing_files: Dict[str, List[str]], operation: str = "download") -> str:
|
|
96
96
|
"""
|
|
97
97
|
Prompt user for confirmation when files already exist (legacy, confirms all files together)
|
|
98
98
|
|
|
@@ -103,11 +103,11 @@ class FileExistenceManager:
|
|
|
103
103
|
Returns:
|
|
104
104
|
User choice: 'use' (use existing), 'overwrite' (regenerate), or 'cancel'
|
|
105
105
|
"""
|
|
106
|
-
has_media = bool(existing_files.get(
|
|
107
|
-
has_subtitle = bool(existing_files.get(
|
|
106
|
+
has_media = bool(existing_files.get("media", []))
|
|
107
|
+
has_subtitle = bool(existing_files.get("subtitle", []))
|
|
108
108
|
|
|
109
109
|
if not has_media and not has_subtitle:
|
|
110
|
-
return
|
|
110
|
+
return "proceed" # No existing files, proceed normally
|
|
111
111
|
|
|
112
112
|
# Header with warning color
|
|
113
113
|
print(f'\n{colorful.bold_yellow("⚠️ Existing files found:")}')
|
|
@@ -115,15 +115,15 @@ class FileExistenceManager:
|
|
|
115
115
|
# Collect file paths for options
|
|
116
116
|
file_paths = []
|
|
117
117
|
if has_media:
|
|
118
|
-
file_paths.extend(existing_files[
|
|
118
|
+
file_paths.extend(existing_files["media"])
|
|
119
119
|
if has_subtitle:
|
|
120
|
-
file_paths.extend(existing_files[
|
|
120
|
+
file_paths.extend(existing_files["subtitle"])
|
|
121
121
|
|
|
122
122
|
# Create display options with emojis
|
|
123
123
|
options = []
|
|
124
124
|
for file_path in file_paths:
|
|
125
125
|
# Determine emoji based on file type
|
|
126
|
-
if has_media and file_path in existing_files[
|
|
126
|
+
if has_media and file_path in existing_files["media"]:
|
|
127
127
|
display_text = f'{colorful.green("•")} 🎬 Media file: {file_path}'
|
|
128
128
|
else:
|
|
129
129
|
display_text = f'{colorful.green("•")} 📝 Subtitle file: {file_path}'
|
|
@@ -132,18 +132,18 @@ class FileExistenceManager:
|
|
|
132
132
|
# Add overwrite and cancel options
|
|
133
133
|
options.extend(
|
|
134
134
|
[
|
|
135
|
-
(
|
|
136
|
-
(
|
|
135
|
+
(" Overwrite existing files (re-generate or download)", "overwrite"),
|
|
136
|
+
(" Cancel operation", "cancel"),
|
|
137
137
|
]
|
|
138
138
|
)
|
|
139
139
|
|
|
140
|
-
prompt_message =
|
|
141
|
-
default_value = file_paths[0] if file_paths else
|
|
140
|
+
prompt_message = "What would you like to do?"
|
|
141
|
+
default_value = file_paths[0] if file_paths else "use"
|
|
142
142
|
choice = FileExistenceManager._prompt_user_choice(prompt_message, options, default=default_value)
|
|
143
143
|
|
|
144
|
-
if choice ==
|
|
144
|
+
if choice == "overwrite":
|
|
145
145
|
print(f'{colorful.yellow("🔄 Overwriting existing files")}')
|
|
146
|
-
elif choice ==
|
|
146
|
+
elif choice == "cancel":
|
|
147
147
|
print(f'{colorful.red("❌ Operation cancelled")}')
|
|
148
148
|
elif choice in file_paths:
|
|
149
149
|
print(f'{colorful.green(f"✅ Using selected file: {choice}")}')
|
|
@@ -153,7 +153,7 @@ class FileExistenceManager:
|
|
|
153
153
|
return choice
|
|
154
154
|
|
|
155
155
|
@staticmethod
|
|
156
|
-
def prompt_file_type_confirmation(file_type: str, files: List[str], operation: str =
|
|
156
|
+
def prompt_file_type_confirmation(file_type: str, files: List[str], operation: str = "download") -> str:
|
|
157
157
|
"""
|
|
158
158
|
Prompt user for confirmation for a specific file type
|
|
159
159
|
|
|
@@ -166,9 +166,9 @@ class FileExistenceManager:
|
|
|
166
166
|
User choice: 'use' (use existing), 'overwrite' (regenerate), or 'cancel'
|
|
167
167
|
"""
|
|
168
168
|
if not files:
|
|
169
|
-
return
|
|
169
|
+
return "proceed"
|
|
170
170
|
|
|
171
|
-
emoji, label = FileExistenceManager.FILE_TYPE_INFO.get(file_type, (
|
|
171
|
+
emoji, label = FileExistenceManager.FILE_TYPE_INFO.get(file_type, ("📄", file_type.capitalize()))
|
|
172
172
|
del emoji # Unused variable
|
|
173
173
|
|
|
174
174
|
# Header with warning color
|
|
@@ -177,26 +177,26 @@ class FileExistenceManager:
|
|
|
177
177
|
for file_path in files:
|
|
178
178
|
print(f' {colorful.green("•")} {file_path}')
|
|
179
179
|
|
|
180
|
-
prompt_message = f
|
|
180
|
+
prompt_message = f"What would you like to do with {label.lower()} files?"
|
|
181
181
|
options = [
|
|
182
|
-
(f
|
|
183
|
-
(f
|
|
184
|
-
(
|
|
182
|
+
(f"Use existing {label.lower()} files (skip {operation})", "use"),
|
|
183
|
+
(f"Overwrite {label.lower()} files (re-{operation})", "overwrite"),
|
|
184
|
+
("Cancel operation", "cancel"),
|
|
185
185
|
]
|
|
186
|
-
choice = FileExistenceManager._prompt_user_choice(prompt_message, options, default=
|
|
186
|
+
choice = FileExistenceManager._prompt_user_choice(prompt_message, options, default="use")
|
|
187
187
|
|
|
188
|
-
if choice ==
|
|
188
|
+
if choice == "use":
|
|
189
189
|
print(f'{colorful.green(f"✅ Using existing {label.lower()} files")}')
|
|
190
|
-
elif choice ==
|
|
190
|
+
elif choice == "overwrite":
|
|
191
191
|
print(f'{colorful.yellow(f"🔄 Overwriting {label.lower()} files")}')
|
|
192
|
-
elif choice ==
|
|
192
|
+
elif choice == "cancel":
|
|
193
193
|
print(f'{colorful.red("❌ Operation cancelled")}')
|
|
194
194
|
|
|
195
195
|
return choice
|
|
196
196
|
|
|
197
197
|
@staticmethod
|
|
198
198
|
def prompt_file_selection(
|
|
199
|
-
file_type: str, files: List[str], operation: str =
|
|
199
|
+
file_type: str, files: List[str], operation: str = "use", enable_gemini: bool = False
|
|
200
200
|
) -> str:
|
|
201
201
|
"""
|
|
202
202
|
Prompt user to select a specific file from a list, or choose to overwrite/cancel
|
|
@@ -211,7 +211,7 @@ class FileExistenceManager:
|
|
|
211
211
|
Selected file path, 'overwrite' to regenerate, 'gemini' to transcribe with Gemini, or 'cancel' to abort
|
|
212
212
|
"""
|
|
213
213
|
if not files:
|
|
214
|
-
return
|
|
214
|
+
return "proceed"
|
|
215
215
|
|
|
216
216
|
# If only one file, simplify the choice
|
|
217
217
|
if len(files) == 1:
|
|
@@ -220,7 +220,7 @@ class FileExistenceManager:
|
|
|
220
220
|
file_type=file_type, files=files, operation=operation
|
|
221
221
|
)
|
|
222
222
|
if files
|
|
223
|
-
else
|
|
223
|
+
else "proceed"
|
|
224
224
|
)
|
|
225
225
|
|
|
226
226
|
# Multiple files: let user choose which one
|
|
@@ -230,24 +230,24 @@ class FileExistenceManager:
|
|
|
230
230
|
options = []
|
|
231
231
|
for i, file_path in enumerate(files, 1):
|
|
232
232
|
# Display full path for clarity
|
|
233
|
-
options.append((f
|
|
233
|
+
options.append((f"{colorful.cyan(file_path)}", file_path))
|
|
234
234
|
|
|
235
235
|
# Add Gemini transcription option if enabled
|
|
236
236
|
if enable_gemini:
|
|
237
|
-
options.append((colorful.magenta(
|
|
237
|
+
options.append((colorful.magenta("✨ Transcribe with Gemini 2.5 Pro"), "gemini"))
|
|
238
238
|
|
|
239
239
|
# Add overwrite and cancel options
|
|
240
|
-
options.append((colorful.yellow(f
|
|
241
|
-
options.append((colorful.red(
|
|
240
|
+
options.append((colorful.yellow(f"Overwrite (re-{operation} or download)"), "overwrite"))
|
|
241
|
+
options.append((colorful.red("Cancel operation"), "cancel"))
|
|
242
242
|
|
|
243
|
-
prompt_message = colorful.bold_black_on_cyan(f
|
|
243
|
+
prompt_message = colorful.bold_black_on_cyan(f"Select which {file_type} to use:")
|
|
244
244
|
choice = FileExistenceManager._prompt_user_choice(prompt_message, options, default=files[0])
|
|
245
245
|
|
|
246
|
-
if choice ==
|
|
246
|
+
if choice == "cancel":
|
|
247
247
|
print(f'{colorful.red("❌ Operation cancelled")}')
|
|
248
|
-
elif choice ==
|
|
248
|
+
elif choice == "overwrite":
|
|
249
249
|
print(f'{colorful.yellow(f"🔄 Overwriting all {file_type} files")}')
|
|
250
|
-
elif choice ==
|
|
250
|
+
elif choice == "gemini":
|
|
251
251
|
print(f'{colorful.magenta("✨ Will transcribe with Gemini 2.5 Pro")}')
|
|
252
252
|
else:
|
|
253
253
|
print(f'{colorful.green(f"✅ Using: {choice}")}')
|
|
@@ -256,7 +256,7 @@ class FileExistenceManager:
|
|
|
256
256
|
|
|
257
257
|
@staticmethod
|
|
258
258
|
def prompt_per_file_type_confirmation(
|
|
259
|
-
existing_files: Dict[str, List[str]], operation: str =
|
|
259
|
+
existing_files: Dict[str, List[str]], operation: str = "download"
|
|
260
260
|
) -> Dict[str, str]:
|
|
261
261
|
"""
|
|
262
262
|
Prompt user for confirmation for each file type, combining interactive selections when possible.
|
|
@@ -269,7 +269,7 @@ class FileExistenceManager:
|
|
|
269
269
|
Dictionary mapping file type to user choice ('use', 'overwrite', 'proceed', or 'cancel')
|
|
270
270
|
"""
|
|
271
271
|
ordered_types = []
|
|
272
|
-
for preferred in [
|
|
272
|
+
for preferred in ["media", "audio", "video", "subtitle"]:
|
|
273
273
|
if preferred not in ordered_types:
|
|
274
274
|
ordered_types.append(preferred)
|
|
275
275
|
for file_type in existing_files.keys():
|
|
@@ -277,7 +277,7 @@ class FileExistenceManager:
|
|
|
277
277
|
ordered_types.append(file_type)
|
|
278
278
|
|
|
279
279
|
file_types_with_files = [ft for ft in ordered_types if existing_files.get(ft)]
|
|
280
|
-
choices = {ft:
|
|
280
|
+
choices = {ft: "proceed" for ft in ordered_types}
|
|
281
281
|
|
|
282
282
|
if not file_types_with_files:
|
|
283
283
|
return choices
|
|
@@ -292,9 +292,9 @@ class FileExistenceManager:
|
|
|
292
292
|
for file_type in file_types_with_files:
|
|
293
293
|
choice = FileExistenceManager.prompt_file_type_confirmation(file_type, existing_files[file_type], operation)
|
|
294
294
|
choices[file_type] = choice
|
|
295
|
-
if choice ==
|
|
295
|
+
if choice == "cancel":
|
|
296
296
|
for remaining in file_types_with_files[file_types_with_files.index(file_type) + 1 :]:
|
|
297
|
-
choices[remaining] =
|
|
297
|
+
choices[remaining] = "cancel"
|
|
298
298
|
break
|
|
299
299
|
|
|
300
300
|
return choices
|
|
@@ -339,7 +339,7 @@ class FileExistenceManager:
|
|
|
339
339
|
default=default,
|
|
340
340
|
).ask()
|
|
341
341
|
except (KeyboardInterrupt, EOFError):
|
|
342
|
-
return
|
|
342
|
+
return "cancel"
|
|
343
343
|
except Exception:
|
|
344
344
|
selection = None
|
|
345
345
|
|
|
@@ -347,7 +347,7 @@ class FileExistenceManager:
|
|
|
347
347
|
return selection
|
|
348
348
|
if default:
|
|
349
349
|
return default
|
|
350
|
-
return
|
|
350
|
+
return "cancel"
|
|
351
351
|
|
|
352
352
|
return FileExistenceManager._prompt_with_numeric_input(prompt_message, options, default)
|
|
353
353
|
|
|
@@ -358,17 +358,17 @@ class FileExistenceManager:
|
|
|
358
358
|
default: str = None,
|
|
359
359
|
) -> str:
|
|
360
360
|
numbered_choices = {str(index + 1): value for index, (_, value) in enumerate(options)}
|
|
361
|
-
label_lines = [f
|
|
362
|
-
prompt_header = f
|
|
361
|
+
label_lines = [f"{index + 1}. {label}" for index, (label, _) in enumerate(options)]
|
|
362
|
+
prompt_header = f"{prompt_message}"
|
|
363
363
|
print(prompt_header)
|
|
364
364
|
for line in label_lines:
|
|
365
365
|
print(line)
|
|
366
366
|
|
|
367
367
|
while True:
|
|
368
368
|
try:
|
|
369
|
-
raw_choice = input(f
|
|
369
|
+
raw_choice = input(f"\nEnter your choice (1-{len(options)}): ").strip()
|
|
370
370
|
except (EOFError, KeyboardInterrupt):
|
|
371
|
-
return
|
|
371
|
+
return "cancel"
|
|
372
372
|
|
|
373
373
|
if not raw_choice and default:
|
|
374
374
|
return default
|
|
@@ -376,7 +376,7 @@ class FileExistenceManager:
|
|
|
376
376
|
if raw_choice in numbered_choices:
|
|
377
377
|
return numbered_choices[raw_choice]
|
|
378
378
|
|
|
379
|
-
print(
|
|
379
|
+
print("Invalid choice. Please enter one of the displayed numbers.")
|
|
380
380
|
|
|
381
381
|
@staticmethod
|
|
382
382
|
def _combined_file_type_prompt(
|
|
@@ -404,7 +404,7 @@ class FileExistenceManager:
|
|
|
404
404
|
file_types: Sequence[str],
|
|
405
405
|
operation: str,
|
|
406
406
|
) -> Optional[Dict[str, str]]:
|
|
407
|
-
states = {file_type:
|
|
407
|
+
states = {file_type: "use" for file_type in file_types}
|
|
408
408
|
selected_index = 0
|
|
409
409
|
total_items = len(file_types) + 2 # file types + confirm + cancel
|
|
410
410
|
total_lines = len(file_types) + 4 # prompt + items + confirm/cancel + instructions
|
|
@@ -414,7 +414,7 @@ class FileExistenceManager:
|
|
|
414
414
|
num_mapping = {str(index + 1): index for index in range(len(file_types))}
|
|
415
415
|
|
|
416
416
|
try:
|
|
417
|
-
if os.name ==
|
|
417
|
+
if os.name == "nt":
|
|
418
418
|
read_key = FileExistenceManager._read_key_windows
|
|
419
419
|
raw_mode = _NullContext()
|
|
420
420
|
else:
|
|
@@ -424,20 +424,20 @@ class FileExistenceManager:
|
|
|
424
424
|
with raw_mode:
|
|
425
425
|
while True:
|
|
426
426
|
key = read_key()
|
|
427
|
-
if key ==
|
|
427
|
+
if key == "up":
|
|
428
428
|
selected_index = (selected_index - 1) % total_items
|
|
429
429
|
FileExistenceManager._refresh_combined_file_menu(
|
|
430
430
|
total_lines, existing_files, file_types, states, selected_index, operation
|
|
431
431
|
)
|
|
432
|
-
elif key ==
|
|
432
|
+
elif key == "down":
|
|
433
433
|
selected_index = (selected_index + 1) % total_items
|
|
434
434
|
FileExistenceManager._refresh_combined_file_menu(
|
|
435
435
|
total_lines, existing_files, file_types, states, selected_index, operation
|
|
436
436
|
)
|
|
437
|
-
elif key in (
|
|
437
|
+
elif key in ("enter", "space"):
|
|
438
438
|
if selected_index < len(file_types):
|
|
439
439
|
current_type = file_types[selected_index]
|
|
440
|
-
states[current_type] =
|
|
440
|
+
states[current_type] = "overwrite" if states[current_type] == "use" else "use"
|
|
441
441
|
FileExistenceManager._refresh_combined_file_menu(
|
|
442
442
|
total_lines, existing_files, file_types, states, selected_index, operation
|
|
443
443
|
)
|
|
@@ -445,7 +445,7 @@ class FileExistenceManager:
|
|
|
445
445
|
return FileExistenceManager._finalize_combined_states(file_types, states)
|
|
446
446
|
else:
|
|
447
447
|
return FileExistenceManager._cancel_combined_states(file_types)
|
|
448
|
-
elif key ==
|
|
448
|
+
elif key == "cancel":
|
|
449
449
|
return FileExistenceManager._cancel_combined_states(file_types)
|
|
450
450
|
elif key in num_mapping:
|
|
451
451
|
selected_index = num_mapping[key]
|
|
@@ -469,49 +469,49 @@ class FileExistenceManager:
|
|
|
469
469
|
selected_index: int,
|
|
470
470
|
operation: str,
|
|
471
471
|
) -> None:
|
|
472
|
-
prompt = colorful.bold_black_on_cyan(
|
|
472
|
+
prompt = colorful.bold_black_on_cyan("Select how to handle existing files")
|
|
473
473
|
print(prompt)
|
|
474
474
|
|
|
475
475
|
for idx, file_type in enumerate(file_types):
|
|
476
|
-
label = FileExistenceManager.FILE_TYPE_INFO.get(file_type, (
|
|
476
|
+
label = FileExistenceManager.FILE_TYPE_INFO.get(file_type, ("📄", file_type.capitalize()))[1]
|
|
477
477
|
count = len(existing_files.get(file_type, []))
|
|
478
478
|
count_suffix = f' ({count} file{"s" if count != 1 else ""})'
|
|
479
|
-
state_plain =
|
|
479
|
+
state_plain = "Use existing" if states[file_type] == "use" else f"Overwrite ({operation})"
|
|
480
480
|
if idx == selected_index:
|
|
481
|
-
prefix = colorful.bold_white(
|
|
482
|
-
line = colorful.bold_black_on_cyan(f
|
|
481
|
+
prefix = colorful.bold_white(">")
|
|
482
|
+
line = colorful.bold_black_on_cyan(f"{label}: {state_plain}{count_suffix}")
|
|
483
483
|
else:
|
|
484
|
-
prefix =
|
|
485
|
-
if states[file_type] ==
|
|
486
|
-
state_text = colorful.green(
|
|
484
|
+
prefix = " "
|
|
485
|
+
if states[file_type] == "use":
|
|
486
|
+
state_text = colorful.green("Use existing")
|
|
487
487
|
else:
|
|
488
|
-
state_text = colorful.yellow(f
|
|
489
|
-
line = f
|
|
490
|
-
print(f
|
|
488
|
+
state_text = colorful.yellow(f"Overwrite ({operation})")
|
|
489
|
+
line = f"{label}: {state_text}{count_suffix}"
|
|
490
|
+
print(f"{prefix} {line}")
|
|
491
491
|
|
|
492
492
|
confirm_index = len(file_types)
|
|
493
493
|
cancel_index = len(file_types) + 1
|
|
494
494
|
|
|
495
495
|
if selected_index == confirm_index:
|
|
496
|
-
confirm_line = colorful.bold_black_on_cyan(
|
|
497
|
-
confirm_prefix = colorful.bold_white(
|
|
496
|
+
confirm_line = colorful.bold_black_on_cyan("Confirm selections")
|
|
497
|
+
confirm_prefix = colorful.bold_white(">")
|
|
498
498
|
else:
|
|
499
|
-
confirm_line = colorful.bold_green(
|
|
500
|
-
confirm_prefix =
|
|
501
|
-
print(f
|
|
499
|
+
confirm_line = colorful.bold_green("Confirm selections")
|
|
500
|
+
confirm_prefix = " "
|
|
501
|
+
print(f"{confirm_prefix} {confirm_line}")
|
|
502
502
|
|
|
503
503
|
if selected_index == cancel_index:
|
|
504
|
-
cancel_line = colorful.bold_black_on_cyan(
|
|
505
|
-
cancel_prefix = colorful.bold_white(
|
|
504
|
+
cancel_line = colorful.bold_black_on_cyan("Cancel operation")
|
|
505
|
+
cancel_prefix = colorful.bold_white(">")
|
|
506
506
|
else:
|
|
507
|
-
cancel_line = colorful.bold_red(
|
|
508
|
-
cancel_prefix =
|
|
509
|
-
print(f
|
|
507
|
+
cancel_line = colorful.bold_red("Cancel operation")
|
|
508
|
+
cancel_prefix = " "
|
|
509
|
+
print(f"{cancel_prefix} {cancel_line}")
|
|
510
510
|
|
|
511
511
|
print(
|
|
512
|
-
|
|
513
|
-
+ colorful.bold_black_on_cyan(
|
|
514
|
-
+
|
|
512
|
+
"Use "
|
|
513
|
+
+ colorful.bold_black_on_cyan("↑/↓")
|
|
514
|
+
+ " to navigate. Enter/Space toggles an item. Confirm to proceed or cancel to abort."
|
|
515
515
|
)
|
|
516
516
|
|
|
517
517
|
@staticmethod
|
|
@@ -523,58 +523,58 @@ class FileExistenceManager:
|
|
|
523
523
|
selected_index: int,
|
|
524
524
|
operation: str,
|
|
525
525
|
) -> None:
|
|
526
|
-
move_up =
|
|
527
|
-
clear_line =
|
|
526
|
+
move_up = "\033[F" * total_lines
|
|
527
|
+
clear_line = "\033[K"
|
|
528
528
|
sys.stdout.write(move_up)
|
|
529
529
|
sys.stdout.write(clear_line)
|
|
530
530
|
sys.stdout.flush()
|
|
531
531
|
|
|
532
|
-
prompt = colorful.bold_black_on_cyan(
|
|
532
|
+
prompt = colorful.bold_black_on_cyan("Select how to handle existing files")
|
|
533
533
|
print(prompt)
|
|
534
534
|
|
|
535
535
|
for idx, file_type in enumerate(file_types):
|
|
536
536
|
sys.stdout.write(clear_line)
|
|
537
|
-
label = FileExistenceManager.FILE_TYPE_INFO.get(file_type, (
|
|
537
|
+
label = FileExistenceManager.FILE_TYPE_INFO.get(file_type, ("📄", file_type.capitalize()))[1]
|
|
538
538
|
count = len(existing_files.get(file_type, []))
|
|
539
539
|
count_suffix = f' ({count} file{"s" if count != 1 else ""})'
|
|
540
|
-
state_plain =
|
|
540
|
+
state_plain = "Use existing" if states[file_type] == "use" else f"Overwrite ({operation})"
|
|
541
541
|
if idx == selected_index:
|
|
542
|
-
prefix = colorful.bold_white(
|
|
543
|
-
line = colorful.bold_black_on_cyan(f
|
|
542
|
+
prefix = colorful.bold_white(">")
|
|
543
|
+
line = colorful.bold_black_on_cyan(f"{label}: {state_plain}{count_suffix}")
|
|
544
544
|
else:
|
|
545
|
-
prefix =
|
|
546
|
-
if states[file_type] ==
|
|
547
|
-
state_text = colorful.green(
|
|
545
|
+
prefix = " "
|
|
546
|
+
if states[file_type] == "use":
|
|
547
|
+
state_text = colorful.green("Use existing")
|
|
548
548
|
else:
|
|
549
|
-
state_text = colorful.yellow(f
|
|
550
|
-
line = f
|
|
551
|
-
print(f
|
|
549
|
+
state_text = colorful.yellow(f"Overwrite ({operation})")
|
|
550
|
+
line = f"{label}: {state_text}{count_suffix}"
|
|
551
|
+
print(f"{prefix} {line}")
|
|
552
552
|
|
|
553
553
|
sys.stdout.write(clear_line)
|
|
554
554
|
confirm_index = len(file_types)
|
|
555
555
|
cancel_index = len(file_types) + 1
|
|
556
556
|
if selected_index == confirm_index:
|
|
557
|
-
confirm_line = colorful.bold_black_on_cyan(
|
|
558
|
-
confirm_prefix = colorful.bold_white(
|
|
557
|
+
confirm_line = colorful.bold_black_on_cyan("Confirm selections")
|
|
558
|
+
confirm_prefix = colorful.bold_white(">")
|
|
559
559
|
else:
|
|
560
|
-
confirm_line = colorful.bold_green(
|
|
561
|
-
confirm_prefix =
|
|
562
|
-
print(f
|
|
560
|
+
confirm_line = colorful.bold_green("Confirm selections")
|
|
561
|
+
confirm_prefix = " "
|
|
562
|
+
print(f"{confirm_prefix} {confirm_line}")
|
|
563
563
|
|
|
564
564
|
sys.stdout.write(clear_line)
|
|
565
565
|
if selected_index == cancel_index:
|
|
566
|
-
cancel_line = colorful.bold_black_on_cyan(
|
|
567
|
-
cancel_prefix = colorful.bold_white(
|
|
566
|
+
cancel_line = colorful.bold_black_on_cyan("Cancel operation")
|
|
567
|
+
cancel_prefix = colorful.bold_white(">")
|
|
568
568
|
else:
|
|
569
|
-
cancel_line = colorful.bold_red(
|
|
570
|
-
cancel_prefix =
|
|
571
|
-
print(f
|
|
569
|
+
cancel_line = colorful.bold_red("Cancel operation")
|
|
570
|
+
cancel_prefix = " "
|
|
571
|
+
print(f"{cancel_prefix} {cancel_line}")
|
|
572
572
|
|
|
573
573
|
sys.stdout.write(clear_line)
|
|
574
574
|
print(
|
|
575
|
-
|
|
576
|
-
+ colorful.bold_black_on_cyan(
|
|
577
|
-
+
|
|
575
|
+
"Use "
|
|
576
|
+
+ colorful.bold_black_on_cyan("↑/↓")
|
|
577
|
+
+ " to navigate. Enter/Space toggles an item. Confirm to proceed or cancel to abort."
|
|
578
578
|
)
|
|
579
579
|
sys.stdout.flush()
|
|
580
580
|
|
|
@@ -586,41 +586,41 @@ class FileExistenceManager:
|
|
|
586
586
|
) -> Dict[str, str]:
|
|
587
587
|
label_choices = []
|
|
588
588
|
for file_type in file_types:
|
|
589
|
-
label = FileExistenceManager.FILE_TYPE_INFO.get(file_type, (
|
|
589
|
+
label = FileExistenceManager.FILE_TYPE_INFO.get(file_type, ("📄", file_type.capitalize()))[1]
|
|
590
590
|
count = len(existing_files.get(file_type, []))
|
|
591
591
|
count_suffix = f' ({count} file{"s" if count != 1 else ""})'
|
|
592
|
-
label_choices.append(questionary.Choice(title=f
|
|
592
|
+
label_choices.append(questionary.Choice(title=f"{label}{count_suffix}", value=file_type))
|
|
593
593
|
|
|
594
|
-
label_choices.append(questionary.Choice(title=
|
|
594
|
+
label_choices.append(questionary.Choice(title="Cancel operation", value="__cancel__"))
|
|
595
595
|
|
|
596
596
|
try:
|
|
597
597
|
selection = questionary.checkbox(
|
|
598
|
-
message=
|
|
598
|
+
message="Select file types to overwrite (others will use existing files)",
|
|
599
599
|
choices=label_choices,
|
|
600
|
-
instruction=
|
|
600
|
+
instruction="Press Space to toggle overwrite. Press Enter to confirm.",
|
|
601
601
|
).ask()
|
|
602
602
|
except (KeyboardInterrupt, EOFError):
|
|
603
603
|
return FileExistenceManager._cancel_combined_states(file_types)
|
|
604
604
|
|
|
605
|
-
if selection is None or
|
|
605
|
+
if selection is None or "__cancel__" in selection:
|
|
606
606
|
return FileExistenceManager._cancel_combined_states(file_types)
|
|
607
607
|
|
|
608
|
-
states = {file_type: (
|
|
608
|
+
states = {file_type: ("overwrite" if file_type in selection else "use") for file_type in file_types}
|
|
609
609
|
return FileExistenceManager._finalize_combined_states(file_types, states)
|
|
610
610
|
|
|
611
611
|
@staticmethod
|
|
612
612
|
def _finalize_combined_states(file_types: Sequence[str], states: Dict[str, str]) -> Dict[str, str]:
|
|
613
|
-
return {file_type: (
|
|
613
|
+
return {file_type: ("overwrite" if states.get(file_type) == "overwrite" else "use") for file_type in file_types}
|
|
614
614
|
|
|
615
615
|
@staticmethod
|
|
616
616
|
def _cancel_combined_states(file_types: Sequence[str]) -> Dict[str, str]:
|
|
617
|
-
return {file_type:
|
|
617
|
+
return {file_type: "cancel" for file_type in file_types}
|
|
618
618
|
|
|
619
619
|
@staticmethod
|
|
620
620
|
def _supports_native_selector() -> bool:
|
|
621
621
|
if not sys.stdin.isatty() or not sys.stdout.isatty():
|
|
622
622
|
return False
|
|
623
|
-
if os.name ==
|
|
623
|
+
if os.name == "nt":
|
|
624
624
|
try:
|
|
625
625
|
import msvcrt # noqa: F401
|
|
626
626
|
except ImportError:
|
|
@@ -653,7 +653,7 @@ class FileExistenceManager:
|
|
|
653
653
|
FileExistenceManager._render_menu(prompt_message, options, selected_index)
|
|
654
654
|
|
|
655
655
|
try:
|
|
656
|
-
if os.name ==
|
|
656
|
+
if os.name == "nt":
|
|
657
657
|
read_key = FileExistenceManager._read_key_windows
|
|
658
658
|
raw_mode = _NullContext()
|
|
659
659
|
else:
|
|
@@ -663,20 +663,20 @@ class FileExistenceManager:
|
|
|
663
663
|
with raw_mode:
|
|
664
664
|
while True:
|
|
665
665
|
key = read_key()
|
|
666
|
-
if key ==
|
|
666
|
+
if key == "up":
|
|
667
667
|
selected_index = (selected_index - 1) % len(options)
|
|
668
668
|
FileExistenceManager._refresh_menu(total_lines, prompt_message, options, selected_index)
|
|
669
|
-
elif key ==
|
|
669
|
+
elif key == "down":
|
|
670
670
|
selected_index = (selected_index + 1) % len(options)
|
|
671
671
|
FileExistenceManager._refresh_menu(total_lines, prompt_message, options, selected_index)
|
|
672
|
-
elif key ==
|
|
672
|
+
elif key == "enter":
|
|
673
673
|
return options[selected_index][1]
|
|
674
674
|
elif key in value_by_number:
|
|
675
675
|
return value_by_number[key]
|
|
676
|
-
elif key ==
|
|
677
|
-
return
|
|
676
|
+
elif key == "cancel":
|
|
677
|
+
return "cancel"
|
|
678
678
|
except KeyboardInterrupt:
|
|
679
|
-
return
|
|
679
|
+
return "cancel"
|
|
680
680
|
except Exception:
|
|
681
681
|
return FileExistenceManager._prompt_with_numeric_input(prompt_message, options, default)
|
|
682
682
|
finally:
|
|
@@ -684,17 +684,17 @@ class FileExistenceManager:
|
|
|
684
684
|
|
|
685
685
|
@staticmethod
|
|
686
686
|
def _render_menu(prompt_message: str, options: Sequence[Tuple[str, str]], selected_index: int) -> None:
|
|
687
|
-
prompt = f
|
|
687
|
+
prompt = f"{prompt_message}"
|
|
688
688
|
print(prompt)
|
|
689
689
|
for idx, (label, _) in enumerate(options):
|
|
690
690
|
if idx == selected_index:
|
|
691
|
-
prefix = colorful.bold_white(
|
|
691
|
+
prefix = colorful.bold_white(">")
|
|
692
692
|
suffix = colorful.bold_black_on_cyan(str(label))
|
|
693
693
|
else:
|
|
694
|
-
prefix =
|
|
694
|
+
prefix = " "
|
|
695
695
|
suffix = label
|
|
696
|
-
print(f
|
|
697
|
-
print(
|
|
696
|
+
print(f"{prefix} {suffix}")
|
|
697
|
+
print("Use " + colorful.bold_black_on_cyan("↑/↓") + " to move, Enter to confirm, or press a number to choose.")
|
|
698
698
|
|
|
699
699
|
@staticmethod
|
|
700
700
|
def _refresh_menu(
|
|
@@ -703,24 +703,24 @@ class FileExistenceManager:
|
|
|
703
703
|
options: Sequence[Tuple[str, str]],
|
|
704
704
|
selected_index: int,
|
|
705
705
|
) -> None:
|
|
706
|
-
move_up =
|
|
707
|
-
clear_line =
|
|
706
|
+
move_up = "\033[F" * total_lines
|
|
707
|
+
clear_line = "\033[K"
|
|
708
708
|
sys.stdout.write(move_up)
|
|
709
709
|
sys.stdout.write(clear_line)
|
|
710
710
|
sys.stdout.flush()
|
|
711
|
-
prompt = f
|
|
711
|
+
prompt = f"{prompt_message}"
|
|
712
712
|
print(prompt)
|
|
713
713
|
for idx, (label, _) in enumerate(options):
|
|
714
714
|
sys.stdout.write(clear_line)
|
|
715
715
|
if idx == selected_index:
|
|
716
|
-
prefix = colorful.bold_white(
|
|
716
|
+
prefix = colorful.bold_white(">")
|
|
717
717
|
suffix = colorful.bold_black_on_cyan(str(label))
|
|
718
718
|
else:
|
|
719
|
-
prefix =
|
|
719
|
+
prefix = " "
|
|
720
720
|
suffix = label
|
|
721
|
-
print(f
|
|
721
|
+
print(f"{prefix} {suffix}")
|
|
722
722
|
sys.stdout.write(clear_line)
|
|
723
|
-
print(
|
|
723
|
+
print("Use " + colorful.bold_black_on_cyan("↑/↓") + " to move, Enter to confirm, or press a number to choose.")
|
|
724
724
|
sys.stdout.flush()
|
|
725
725
|
|
|
726
726
|
@staticmethod
|
|
@@ -729,53 +729,53 @@ class FileExistenceManager:
|
|
|
729
729
|
|
|
730
730
|
while True:
|
|
731
731
|
ch = msvcrt.getwch()
|
|
732
|
-
if ch in (
|
|
732
|
+
if ch in ("\x00", "\xe0"):
|
|
733
733
|
extended = msvcrt.getwch()
|
|
734
|
-
if extended ==
|
|
735
|
-
return
|
|
736
|
-
if extended ==
|
|
737
|
-
return
|
|
734
|
+
if extended == "H":
|
|
735
|
+
return "up"
|
|
736
|
+
if extended == "P":
|
|
737
|
+
return "down"
|
|
738
738
|
continue
|
|
739
|
-
if ch in (
|
|
740
|
-
return
|
|
741
|
-
if ch ==
|
|
742
|
-
return
|
|
739
|
+
if ch in ("\r", "\n"):
|
|
740
|
+
return "enter"
|
|
741
|
+
if ch == " ":
|
|
742
|
+
return "space"
|
|
743
743
|
if ch.isdigit():
|
|
744
744
|
return ch
|
|
745
|
-
if ch.lower() in (
|
|
746
|
-
return
|
|
747
|
-
if ch ==
|
|
748
|
-
return
|
|
749
|
-
if ch ==
|
|
745
|
+
if ch.lower() in ("j", "k"):
|
|
746
|
+
return "down" if ch.lower() == "j" else "up"
|
|
747
|
+
if ch == "\x1b":
|
|
748
|
+
return "cancel"
|
|
749
|
+
if ch == "\x03":
|
|
750
750
|
raise KeyboardInterrupt
|
|
751
751
|
|
|
752
752
|
@staticmethod
|
|
753
753
|
def _read_key_posix() -> str:
|
|
754
754
|
ch = sys.stdin.read(1)
|
|
755
|
-
if ch ==
|
|
755
|
+
if ch == "\x1b":
|
|
756
756
|
seq = sys.stdin.read(2)
|
|
757
|
-
if seq ==
|
|
758
|
-
return
|
|
759
|
-
if seq ==
|
|
760
|
-
return
|
|
761
|
-
return
|
|
762
|
-
if ch in (
|
|
763
|
-
return
|
|
764
|
-
if ch ==
|
|
765
|
-
return
|
|
757
|
+
if seq == "[A":
|
|
758
|
+
return "up"
|
|
759
|
+
if seq == "[B":
|
|
760
|
+
return "down"
|
|
761
|
+
return "cancel"
|
|
762
|
+
if ch in ("\r", "\n"):
|
|
763
|
+
return "enter"
|
|
764
|
+
if ch == " ":
|
|
765
|
+
return "space"
|
|
766
766
|
if ch.isdigit():
|
|
767
767
|
return ch
|
|
768
|
-
if ch.lower() ==
|
|
769
|
-
return
|
|
770
|
-
if ch.lower() ==
|
|
771
|
-
return
|
|
772
|
-
if ch ==
|
|
768
|
+
if ch.lower() == "j":
|
|
769
|
+
return "down"
|
|
770
|
+
if ch.lower() == "k":
|
|
771
|
+
return "up"
|
|
772
|
+
if ch == "\x03":
|
|
773
773
|
raise KeyboardInterrupt
|
|
774
|
-
return
|
|
774
|
+
return ""
|
|
775
775
|
|
|
776
776
|
@staticmethod
|
|
777
777
|
def _stdin_raw_mode():
|
|
778
|
-
if os.name ==
|
|
778
|
+
if os.name == "nt":
|
|
779
779
|
return _NullContext()
|
|
780
780
|
|
|
781
781
|
import termios
|