lattifai 0.4.5__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.
@@ -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
- 'media': ('🎬', 'Media'),
24
+ "media": ("🎬", "Media"),
25
25
  # 'audio': ('📱', 'Audio'),
26
26
  # 'video': ('🎬', 'Video'),
27
- 'subtitle': ('📝', 'Subtitle'),
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 = {'media': [], 'subtitle': []}
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 ['mp3', 'wav', 'm4a', 'aac', 'opus', 'mp4', 'webm', 'mkv', 'avi']
57
- subtitle_formats = subtitle_formats or ['md', 'srt', 'vtt', 'ass', 'ssa', 'sub', 'sbv', 'txt']
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'{video_id}.{ext}'
62
+ media_file = output_path / f"{video_id}.{ext}"
63
63
  if media_file.exists():
64
- existing_files['media'].append(str(media_file))
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'{video_id}*.{ext}'):
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['media']:
70
- existing_files['media'].append(file_path)
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'{video_id}.{ext}'
76
+ subtitle_file = output_path / f"{video_id}.{ext}"
77
77
  if subtitle_file.exists():
78
- existing_files['subtitle'].append(str(subtitle_file))
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'{video_id}.*.{ext}'):
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['subtitle']:
84
- existing_files['subtitle'].append(file_path)
83
+ if file_path not in existing_files["subtitle"]:
84
+ existing_files["subtitle"].append(file_path)
85
85
 
86
- if 'md' in subtitle_formats:
86
+ if "md" in subtitle_formats:
87
87
  # Gemini-specific pattern: {video_id}_Gemini.md
88
- gemini_subtitle_file = output_path / f'{video_id}_Gemini.md'
88
+ gemini_subtitle_file = output_path / f"{video_id}_Gemini.md"
89
89
  if gemini_subtitle_file.exists():
90
- existing_files['subtitle'].append(str(gemini_subtitle_file))
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 = 'download') -> 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('media', []))
107
- has_subtitle = bool(existing_files.get('subtitle', []))
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 'proceed' # No existing files, proceed normally
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['media'])
118
+ file_paths.extend(existing_files["media"])
119
119
  if has_subtitle:
120
- file_paths.extend(existing_files['subtitle'])
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['media']:
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
- (' Overwrite existing files (re-generate or download)', 'overwrite'),
136
- (' Cancel operation', 'cancel'),
135
+ (" Overwrite existing files (re-generate or download)", "overwrite"),
136
+ (" Cancel operation", "cancel"),
137
137
  ]
138
138
  )
139
139
 
140
- prompt_message = 'What would you like to do?'
141
- default_value = file_paths[0] if file_paths else 'use'
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 == 'overwrite':
144
+ if choice == "overwrite":
145
145
  print(f'{colorful.yellow("🔄 Overwriting existing files")}')
146
- elif choice == 'cancel':
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 = 'download') -> 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 'proceed'
169
+ return "proceed"
170
170
 
171
- emoji, label = FileExistenceManager.FILE_TYPE_INFO.get(file_type, ('📄', file_type.capitalize()))
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'What would you like to do with {label.lower()} files?'
180
+ prompt_message = f"What would you like to do with {label.lower()} files?"
181
181
  options = [
182
- (f'Use existing {label.lower()} files (skip {operation})', 'use'),
183
- (f'Overwrite {label.lower()} files (re-{operation})', 'overwrite'),
184
- ('Cancel operation', 'cancel'),
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='use')
186
+ choice = FileExistenceManager._prompt_user_choice(prompt_message, options, default="use")
187
187
 
188
- if choice == 'use':
188
+ if choice == "use":
189
189
  print(f'{colorful.green(f"✅ Using existing {label.lower()} files")}')
190
- elif choice == 'overwrite':
190
+ elif choice == "overwrite":
191
191
  print(f'{colorful.yellow(f"🔄 Overwriting {label.lower()} files")}')
192
- elif choice == 'cancel':
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 = 'use', enable_gemini: bool = False
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 'proceed'
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 'proceed'
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'{colorful.cyan(file_path)}', file_path))
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('✨ Transcribe with Gemini 2.5 Pro'), 'gemini'))
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'Overwrite (re-{operation} or download)'), 'overwrite'))
241
- options.append((colorful.red('Cancel operation'), 'cancel'))
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'Select which {file_type} to use:')
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 == 'cancel':
246
+ if choice == "cancel":
247
247
  print(f'{colorful.red("❌ Operation cancelled")}')
248
- elif choice == 'overwrite':
248
+ elif choice == "overwrite":
249
249
  print(f'{colorful.yellow(f"🔄 Overwriting all {file_type} files")}')
250
- elif choice == 'gemini':
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 = 'download'
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 ['media', 'audio', 'video', 'subtitle']:
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: 'proceed' for ft in ordered_types}
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 == 'cancel':
295
+ if choice == "cancel":
296
296
  for remaining in file_types_with_files[file_types_with_files.index(file_type) + 1 :]:
297
- choices[remaining] = 'cancel'
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 'cancel'
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 'cancel'
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'{index + 1}. {label}' for index, (label, _) in enumerate(options)]
362
- prompt_header = f'{prompt_message}'
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'\nEnter your choice (1-{len(options)}): ').strip()
369
+ raw_choice = input(f"\nEnter your choice (1-{len(options)}): ").strip()
370
370
  except (EOFError, KeyboardInterrupt):
371
- return 'cancel'
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('Invalid choice. Please enter one of the displayed numbers.')
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: 'use' for file_type in file_types}
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 == 'nt':
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 == 'up':
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 == 'down':
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 ('enter', 'space'):
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] = 'overwrite' if states[current_type] == 'use' else 'use'
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 == 'cancel':
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('Select how to handle existing files')
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, ('📄', file_type.capitalize()))[1]
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 = 'Use existing' if states[file_type] == 'use' else f'Overwrite ({operation})'
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'{label}: {state_plain}{count_suffix}')
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] == 'use':
486
- state_text = colorful.green('Use existing')
484
+ prefix = " "
485
+ if states[file_type] == "use":
486
+ state_text = colorful.green("Use existing")
487
487
  else:
488
- state_text = colorful.yellow(f'Overwrite ({operation})')
489
- line = f'{label}: {state_text}{count_suffix}'
490
- print(f'{prefix} {line}')
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('Confirm selections')
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('Confirm selections')
500
- confirm_prefix = ' '
501
- print(f'{confirm_prefix} {confirm_line}')
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('Cancel operation')
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('Cancel operation')
508
- cancel_prefix = ' '
509
- print(f'{cancel_prefix} {cancel_line}')
507
+ cancel_line = colorful.bold_red("Cancel operation")
508
+ cancel_prefix = " "
509
+ print(f"{cancel_prefix} {cancel_line}")
510
510
 
511
511
  print(
512
- 'Use '
513
- + colorful.bold_black_on_cyan('↑/↓')
514
- + ' to navigate. Enter/Space toggles an item. Confirm to proceed or cancel to abort.'
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 = '\033[F' * total_lines
527
- clear_line = '\033[K'
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('Select how to handle existing files')
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, ('📄', file_type.capitalize()))[1]
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 = 'Use existing' if states[file_type] == 'use' else f'Overwrite ({operation})'
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'{label}: {state_plain}{count_suffix}')
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] == 'use':
547
- state_text = colorful.green('Use existing')
545
+ prefix = " "
546
+ if states[file_type] == "use":
547
+ state_text = colorful.green("Use existing")
548
548
  else:
549
- state_text = colorful.yellow(f'Overwrite ({operation})')
550
- line = f'{label}: {state_text}{count_suffix}'
551
- print(f'{prefix} {line}')
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('Confirm selections')
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('Confirm selections')
561
- confirm_prefix = ' '
562
- print(f'{confirm_prefix} {confirm_line}')
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('Cancel operation')
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('Cancel operation')
570
- cancel_prefix = ' '
571
- print(f'{cancel_prefix} {cancel_line}')
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
- 'Use '
576
- + colorful.bold_black_on_cyan('↑/↓')
577
- + ' to navigate. Enter/Space toggles an item. Confirm to proceed or cancel to abort.'
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, ('📄', file_type.capitalize()))[1]
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'{label}{count_suffix}', value=file_type))
592
+ label_choices.append(questionary.Choice(title=f"{label}{count_suffix}", value=file_type))
593
593
 
594
- label_choices.append(questionary.Choice(title='Cancel operation', value='__cancel__'))
594
+ label_choices.append(questionary.Choice(title="Cancel operation", value="__cancel__"))
595
595
 
596
596
  try:
597
597
  selection = questionary.checkbox(
598
- message='Select file types to overwrite (others will use existing files)',
598
+ message="Select file types to overwrite (others will use existing files)",
599
599
  choices=label_choices,
600
- instruction='Press Space to toggle overwrite. Press Enter to confirm.',
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 '__cancel__' in selection:
605
+ if selection is None or "__cancel__" in selection:
606
606
  return FileExistenceManager._cancel_combined_states(file_types)
607
607
 
608
- states = {file_type: ('overwrite' if file_type in selection else 'use') for file_type in file_types}
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: ('overwrite' if states.get(file_type) == 'overwrite' else 'use') for file_type in file_types}
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: 'cancel' for file_type in file_types}
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 == 'nt':
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 == 'nt':
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 == 'up':
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 == 'down':
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 == 'enter':
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 == 'cancel':
677
- return 'cancel'
676
+ elif key == "cancel":
677
+ return "cancel"
678
678
  except KeyboardInterrupt:
679
- return 'cancel'
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'{prompt_message}'
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'{prefix} {suffix}')
697
- print('Use ' + colorful.bold_black_on_cyan('↑/↓') + ' to move, Enter to confirm, or press a number to choose.')
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 = '\033[F' * total_lines
707
- clear_line = '\033[K'
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'{prompt_message}'
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'{prefix} {suffix}')
721
+ print(f"{prefix} {suffix}")
722
722
  sys.stdout.write(clear_line)
723
- print('Use ' + colorful.bold_black_on_cyan('↑/↓') + ' to move, Enter to confirm, or press a number to choose.')
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 ('\x00', '\xe0'):
732
+ if ch in ("\x00", "\xe0"):
733
733
  extended = msvcrt.getwch()
734
- if extended == 'H':
735
- return 'up'
736
- if extended == 'P':
737
- return 'down'
734
+ if extended == "H":
735
+ return "up"
736
+ if extended == "P":
737
+ return "down"
738
738
  continue
739
- if ch in ('\r', '\n'):
740
- return 'enter'
741
- if ch == ' ':
742
- return 'space'
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 ('j', 'k'):
746
- return 'down' if ch.lower() == 'j' else 'up'
747
- if ch == '\x1b':
748
- return 'cancel'
749
- if ch == '\x03':
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 == '\x1b':
755
+ if ch == "\x1b":
756
756
  seq = sys.stdin.read(2)
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'
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() == 'j':
769
- return 'down'
770
- if ch.lower() == 'k':
771
- return 'up'
772
- if ch == '\x03':
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 == 'nt':
778
+ if os.name == "nt":
779
779
  return _NullContext()
780
780
 
781
781
  import termios