lattifai 0.4.5__py3-none-any.whl → 1.0.0__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 +61 -47
- lattifai/alignment/__init__.py +6 -0
- lattifai/alignment/lattice1_aligner.py +119 -0
- lattifai/alignment/lattice1_worker.py +185 -0
- lattifai/{tokenizer → alignment}/phonemizer.py +4 -4
- lattifai/alignment/segmenter.py +166 -0
- lattifai/{tokenizer → alignment}/tokenizer.py +244 -169
- lattifai/audio2.py +211 -0
- lattifai/caption/__init__.py +20 -0
- lattifai/caption/caption.py +1275 -0
- lattifai/{io → caption}/gemini_reader.py +30 -30
- lattifai/{io → caption}/gemini_writer.py +17 -17
- lattifai/{io → caption}/supervision.py +4 -3
- lattifai/caption/text_parser.py +145 -0
- lattifai/cli/__init__.py +17 -0
- lattifai/cli/alignment.py +153 -0
- lattifai/cli/caption.py +204 -0
- lattifai/cli/server.py +19 -0
- lattifai/cli/transcribe.py +197 -0
- lattifai/cli/youtube.py +128 -0
- lattifai/client.py +460 -251
- lattifai/config/__init__.py +20 -0
- lattifai/config/alignment.py +73 -0
- lattifai/config/caption.py +178 -0
- lattifai/config/client.py +46 -0
- lattifai/config/diarization.py +67 -0
- lattifai/config/media.py +335 -0
- lattifai/config/transcription.py +84 -0
- lattifai/diarization/__init__.py +5 -0
- lattifai/diarization/lattifai.py +89 -0
- lattifai/errors.py +98 -91
- lattifai/logging.py +116 -0
- lattifai/mixin.py +552 -0
- lattifai/server/app.py +420 -0
- lattifai/transcription/__init__.py +76 -0
- lattifai/transcription/base.py +108 -0
- lattifai/transcription/gemini.py +219 -0
- lattifai/transcription/lattifai.py +103 -0
- lattifai/{workflows → transcription}/prompts/__init__.py +4 -4
- lattifai/types.py +30 -0
- lattifai/utils.py +16 -44
- lattifai/workflow/__init__.py +22 -0
- lattifai/workflow/agents.py +6 -0
- lattifai/{workflows → workflow}/base.py +22 -22
- lattifai/{workflows → workflow}/file_manager.py +239 -215
- lattifai/workflow/youtube.py +564 -0
- lattifai-1.0.0.dist-info/METADATA +736 -0
- lattifai-1.0.0.dist-info/RECORD +52 -0
- {lattifai-0.4.5.dist-info → lattifai-1.0.0.dist-info}/WHEEL +1 -1
- lattifai-1.0.0.dist-info/entry_points.txt +13 -0
- {lattifai-0.4.5.dist-info → lattifai-1.0.0.dist-info}/licenses/LICENSE +1 -1
- lattifai/base_client.py +0 -126
- lattifai/bin/__init__.py +0 -3
- lattifai/bin/agent.py +0 -325
- lattifai/bin/align.py +0 -296
- lattifai/bin/cli_base.py +0 -25
- lattifai/bin/subtitle.py +0 -210
- lattifai/io/__init__.py +0 -42
- lattifai/io/reader.py +0 -85
- lattifai/io/text_parser.py +0 -75
- lattifai/io/utils.py +0 -15
- lattifai/io/writer.py +0 -90
- lattifai/tokenizer/__init__.py +0 -3
- lattifai/workers/__init__.py +0 -3
- lattifai/workers/lattice1_alpha.py +0 -284
- lattifai/workflows/__init__.py +0 -34
- lattifai/workflows/agents.py +0 -10
- lattifai/workflows/gemini.py +0 -167
- lattifai/workflows/prompts/README.md +0 -22
- lattifai/workflows/prompts/gemini/README.md +0 -24
- lattifai/workflows/prompts/gemini/transcription_gem.txt +0 -81
- lattifai/workflows/youtube.py +0 -931
- lattifai-0.4.5.dist-info/METADATA +0 -808
- lattifai-0.4.5.dist-info/RECORD +0 -39
- lattifai-0.4.5.dist-info/entry_points.txt +0 -3
- {lattifai-0.4.5.dist-info → lattifai-1.0.0.dist-info}/top_level.txt +0 -0
|
@@ -17,97 +17,97 @@ except ImportError: # pragma: no cover - optional dependency
|
|
|
17
17
|
questionary = None
|
|
18
18
|
|
|
19
19
|
|
|
20
|
+
TRANSCRIBE_CHOICE = "transcribe"
|
|
21
|
+
|
|
22
|
+
|
|
20
23
|
class FileExistenceManager:
|
|
21
24
|
"""Utility class for handling file existence checks and user confirmations"""
|
|
22
25
|
|
|
23
26
|
FILE_TYPE_INFO = {
|
|
24
|
-
|
|
27
|
+
"media": ("🎬", "Media"),
|
|
25
28
|
# 'audio': ('📱', 'Audio'),
|
|
26
29
|
# 'video': ('🎬', 'Video'),
|
|
27
|
-
|
|
30
|
+
"caption": ("📝", "Caption"),
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
@staticmethod
|
|
31
34
|
def check_existing_files(
|
|
32
35
|
video_id: str,
|
|
33
|
-
|
|
36
|
+
output_path: str,
|
|
34
37
|
media_formats: List[str] = None,
|
|
35
|
-
|
|
38
|
+
caption_formats: List[str] = None,
|
|
36
39
|
) -> Dict[str, List[str]]:
|
|
37
40
|
"""
|
|
38
41
|
Enhanced version to check for existing media files with customizable formats
|
|
39
42
|
|
|
40
43
|
Args:
|
|
41
44
|
video_id: Video ID from any platform
|
|
42
|
-
|
|
45
|
+
output_path: Output directory to check
|
|
43
46
|
media_formats: List of media formats to check (audio and video combined)
|
|
44
|
-
|
|
47
|
+
caption_formats: List of caption formats to check
|
|
45
48
|
|
|
46
49
|
Returns:
|
|
47
|
-
Dictionary with 'media', '
|
|
50
|
+
Dictionary with 'media', 'caption' keys containing lists of existing files
|
|
48
51
|
"""
|
|
49
|
-
output_path = Path(
|
|
50
|
-
existing_files = {
|
|
52
|
+
output_path = Path(output_path).expanduser()
|
|
53
|
+
existing_files = {"media": [], "caption": []}
|
|
51
54
|
|
|
52
55
|
if not output_path.exists():
|
|
53
56
|
return existing_files
|
|
54
57
|
|
|
55
58
|
# Default formats - combine audio and video formats
|
|
56
|
-
media_formats = media_formats or [
|
|
57
|
-
|
|
59
|
+
media_formats = media_formats or ["mp3", "wav", "m4a", "aac", "opus", "mp4", "webm", "mkv", "avi"]
|
|
60
|
+
caption_formats = caption_formats or ["md", "srt", "vtt", "ass", "ssa", "sub", "sbv", "txt"]
|
|
58
61
|
|
|
59
62
|
# Check for media files (audio and video)
|
|
60
63
|
for ext in set(media_formats): # Remove duplicates
|
|
61
64
|
# Pattern 1: Simple pattern like {video_id}.mp3
|
|
62
|
-
media_file = output_path / f
|
|
65
|
+
media_file = output_path / f"{video_id}.{ext}"
|
|
63
66
|
if media_file.exists():
|
|
64
|
-
existing_files[
|
|
67
|
+
existing_files["media"].append(str(media_file))
|
|
65
68
|
|
|
66
69
|
# Pattern 2: With suffix like {video_id}_Edit.mp3 or {video_id}.something.mp3
|
|
67
|
-
for media_file in output_path.glob(f
|
|
70
|
+
for media_file in output_path.glob(f"{video_id}*.{ext}"):
|
|
68
71
|
file_path = str(media_file)
|
|
69
|
-
if file_path not in existing_files[
|
|
70
|
-
existing_files[
|
|
72
|
+
if file_path not in existing_files["media"]:
|
|
73
|
+
existing_files["media"].append(file_path)
|
|
71
74
|
|
|
72
|
-
# Check for
|
|
73
|
-
for ext in set(
|
|
74
|
-
# Check multiple naming patterns for
|
|
75
|
+
# Check for caption files
|
|
76
|
+
for ext in set(caption_formats): # Remove duplicates
|
|
77
|
+
# Check multiple naming patterns for caption files
|
|
75
78
|
# Pattern 1: Simple pattern like {video_id}.vtt
|
|
76
|
-
|
|
77
|
-
if
|
|
78
|
-
existing_files[
|
|
79
|
+
caption_file = output_path / f"{video_id}.{ext}"
|
|
80
|
+
if caption_file.exists():
|
|
81
|
+
existing_files["caption"].append(str(caption_file))
|
|
79
82
|
|
|
80
83
|
# Pattern 2: With language/track suffix like {video_id}.en-trackid.vtt
|
|
81
|
-
for sub_file in output_path.glob(f
|
|
84
|
+
for sub_file in output_path.glob(f"{video_id}*.{ext}"):
|
|
82
85
|
file_path = str(sub_file)
|
|
83
|
-
if file_path not in existing_files[
|
|
84
|
-
existing_files[
|
|
85
|
-
|
|
86
|
-
if 'md' in subtitle_formats:
|
|
87
|
-
# Gemini-specific pattern: {video_id}_Gemini.md
|
|
88
|
-
gemini_subtitle_file = output_path / f'{video_id}_Gemini.md'
|
|
89
|
-
if gemini_subtitle_file.exists():
|
|
90
|
-
existing_files['subtitle'].append(str(gemini_subtitle_file))
|
|
86
|
+
if file_path not in existing_files["caption"]:
|
|
87
|
+
existing_files["caption"].append(file_path)
|
|
91
88
|
|
|
92
89
|
return existing_files
|
|
93
90
|
|
|
94
91
|
@staticmethod
|
|
95
|
-
def prompt_user_confirmation(
|
|
92
|
+
def prompt_user_confirmation(
|
|
93
|
+
existing_files: Dict[str, List[str]], operation: str = "download", transcriber_name: str = None
|
|
94
|
+
) -> str:
|
|
96
95
|
"""
|
|
97
96
|
Prompt user for confirmation when files already exist (legacy, confirms all files together)
|
|
98
97
|
|
|
99
98
|
Args:
|
|
100
99
|
existing_files: Dictionary of existing files
|
|
101
100
|
operation: Type of operation (e.g., "download", "generate")
|
|
101
|
+
transcriber_name: Name of the transcriber to display (e.g., "Gemini_2.5_Pro")
|
|
102
102
|
|
|
103
103
|
Returns:
|
|
104
104
|
User choice: 'use' (use existing), 'overwrite' (regenerate), or 'cancel'
|
|
105
105
|
"""
|
|
106
|
-
has_media = bool(existing_files.get(
|
|
107
|
-
|
|
106
|
+
has_media = bool(existing_files.get("media", []))
|
|
107
|
+
has_caption = bool(existing_files.get("caption", []))
|
|
108
108
|
|
|
109
|
-
if not has_media and not
|
|
110
|
-
return
|
|
109
|
+
if not has_media and not has_caption:
|
|
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,35 +115,52 @@ class FileExistenceManager:
|
|
|
115
115
|
# Collect file paths for options
|
|
116
116
|
file_paths = []
|
|
117
117
|
if has_media:
|
|
118
|
-
file_paths.extend(existing_files[
|
|
119
|
-
if
|
|
120
|
-
file_paths.extend(existing_files[
|
|
118
|
+
file_paths.extend(existing_files["media"])
|
|
119
|
+
if has_caption:
|
|
120
|
+
file_paths.extend(existing_files["caption"])
|
|
121
121
|
|
|
122
122
|
# Create display options with emojis
|
|
123
|
-
options = []
|
|
124
|
-
for file_path in file_paths:
|
|
123
|
+
options, shift_length = [], 0
|
|
124
|
+
for file_path in sorted(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
|
+
shift_length = len("Media file:")
|
|
128
129
|
else:
|
|
129
|
-
display_text = f'{colorful.green("•")} 📝
|
|
130
|
+
display_text = f'{colorful.green("•")} 📝 Caption file: {file_path}'
|
|
131
|
+
shift_length = len("Caption file:")
|
|
130
132
|
options.append((display_text, file_path))
|
|
131
133
|
|
|
132
|
-
# Add overwrite and cancel options
|
|
134
|
+
# Add overwrite and cancel options with aligned spacing
|
|
135
|
+
overwrite_text, overwrite_op = "Overwrite existing files (re-generate or download)", "overwrite"
|
|
136
|
+
if transcriber_name:
|
|
137
|
+
options.append(
|
|
138
|
+
(
|
|
139
|
+
f'{colorful.green("•")} 🔄 {" " * shift_length} {overwrite_text}',
|
|
140
|
+
overwrite_op,
|
|
141
|
+
)
|
|
142
|
+
)
|
|
143
|
+
overwrite_text, overwrite_op = f"Transcribe with {transcriber_name}", TRANSCRIBE_CHOICE
|
|
144
|
+
|
|
133
145
|
options.extend(
|
|
134
146
|
[
|
|
135
|
-
(
|
|
136
|
-
|
|
147
|
+
(
|
|
148
|
+
f'{colorful.green("•")} 🔄 {" " * shift_length} {overwrite_text}',
|
|
149
|
+
overwrite_op,
|
|
150
|
+
),
|
|
151
|
+
(f'{colorful.green("•")} ❌ {" " * shift_length} Cancel operation', "cancel"),
|
|
137
152
|
]
|
|
138
153
|
)
|
|
139
154
|
|
|
140
|
-
prompt_message =
|
|
141
|
-
default_value = file_paths[0] if file_paths else
|
|
155
|
+
prompt_message = "What would you like to do?"
|
|
156
|
+
default_value = file_paths[0] if file_paths else "use"
|
|
142
157
|
choice = FileExistenceManager._prompt_user_choice(prompt_message, options, default=default_value)
|
|
143
158
|
|
|
144
|
-
if choice ==
|
|
159
|
+
if choice == "overwrite":
|
|
145
160
|
print(f'{colorful.yellow("🔄 Overwriting existing files")}')
|
|
146
|
-
elif choice ==
|
|
161
|
+
elif choice == TRANSCRIBE_CHOICE:
|
|
162
|
+
print(f'{colorful.magenta(f"✨ Will transcribe with {transcriber_name}")}')
|
|
163
|
+
elif choice == "cancel":
|
|
147
164
|
print(f'{colorful.red("❌ Operation cancelled")}')
|
|
148
165
|
elif choice in file_paths:
|
|
149
166
|
print(f'{colorful.green(f"✅ Using selected file: {choice}")}')
|
|
@@ -153,12 +170,12 @@ class FileExistenceManager:
|
|
|
153
170
|
return choice
|
|
154
171
|
|
|
155
172
|
@staticmethod
|
|
156
|
-
def prompt_file_type_confirmation(file_type: str, files: List[str], operation: str =
|
|
173
|
+
def prompt_file_type_confirmation(file_type: str, files: List[str], operation: str = "download") -> str:
|
|
157
174
|
"""
|
|
158
175
|
Prompt user for confirmation for a specific file type
|
|
159
176
|
|
|
160
177
|
Args:
|
|
161
|
-
file_type: Type of file ('audio', 'video', '
|
|
178
|
+
file_type: Type of file ('audio', 'video', 'caption', 'gemini')
|
|
162
179
|
files: List of existing files of this type
|
|
163
180
|
operation: Type of operation (e.g., "download", "generate")
|
|
164
181
|
|
|
@@ -166,52 +183,56 @@ class FileExistenceManager:
|
|
|
166
183
|
User choice: 'use' (use existing), 'overwrite' (regenerate), or 'cancel'
|
|
167
184
|
"""
|
|
168
185
|
if not files:
|
|
169
|
-
return
|
|
186
|
+
return "proceed"
|
|
170
187
|
|
|
171
|
-
emoji, label = FileExistenceManager.FILE_TYPE_INFO.get(file_type, (
|
|
188
|
+
emoji, label = FileExistenceManager.FILE_TYPE_INFO.get(file_type, ("📄", file_type.capitalize()))
|
|
172
189
|
del emoji # Unused variable
|
|
173
190
|
|
|
174
191
|
# Header with warning color
|
|
175
192
|
print(f'\n{colorful.bold_yellow(f"⚠️ Existing {label} files found:")}')
|
|
176
193
|
|
|
177
|
-
for file_path in files:
|
|
194
|
+
for file_path in sorted(files):
|
|
178
195
|
print(f' {colorful.green("•")} {file_path}')
|
|
179
196
|
|
|
180
|
-
prompt_message = f
|
|
197
|
+
prompt_message = f"What would you like to do with {label} files?"
|
|
181
198
|
options = [
|
|
182
|
-
(f
|
|
183
|
-
(f
|
|
184
|
-
(
|
|
199
|
+
(f"Use existing {label} files (skip {operation})", "use"),
|
|
200
|
+
(f"Overwrite {label} files (re-{operation})", "overwrite"),
|
|
201
|
+
("Cancel operation", "cancel"),
|
|
185
202
|
]
|
|
186
|
-
choice = FileExistenceManager._prompt_user_choice(prompt_message, options, default=
|
|
203
|
+
choice = FileExistenceManager._prompt_user_choice(prompt_message, options, default="use")
|
|
187
204
|
|
|
188
|
-
if choice ==
|
|
189
|
-
print(f'{colorful.green(f"✅ Using existing {label
|
|
190
|
-
elif choice ==
|
|
191
|
-
print(f'{colorful.yellow(f"🔄 Overwriting {label
|
|
192
|
-
elif choice ==
|
|
205
|
+
if choice == "use":
|
|
206
|
+
print(f'{colorful.green(f"✅ Using existing {label} files")}')
|
|
207
|
+
elif choice == "overwrite":
|
|
208
|
+
print(f'{colorful.yellow(f"🔄 Overwriting {label} files")}')
|
|
209
|
+
elif choice == "cancel":
|
|
193
210
|
print(f'{colorful.red("❌ Operation cancelled")}')
|
|
194
211
|
|
|
195
212
|
return choice
|
|
196
213
|
|
|
197
214
|
@staticmethod
|
|
198
215
|
def prompt_file_selection(
|
|
199
|
-
file_type: str,
|
|
216
|
+
file_type: str,
|
|
217
|
+
files: List[str],
|
|
218
|
+
operation: str = "use",
|
|
219
|
+
transcriber_name: str = None,
|
|
200
220
|
) -> str:
|
|
201
221
|
"""
|
|
202
222
|
Prompt user to select a specific file from a list, or choose to overwrite/cancel
|
|
203
223
|
|
|
204
224
|
Args:
|
|
205
|
-
file_type: Type of file (e.g., 'gemini transcript', '
|
|
225
|
+
file_type: Type of file (e.g., 'gemini transcript', 'caption')
|
|
206
226
|
files: List of existing files to choose from
|
|
207
227
|
operation: Type of operation (e.g., "transcribe", "download")
|
|
208
|
-
|
|
228
|
+
transcriber_name: Name of the transcriber to display (e.g., "Gemini_2.5_Pro").
|
|
229
|
+
If provided, adds transcribe option for the transcriber.
|
|
209
230
|
|
|
210
231
|
Returns:
|
|
211
|
-
Selected file path, 'overwrite' to regenerate, 'gemini' to transcribe with
|
|
232
|
+
Selected file path, 'overwrite' to regenerate, 'gemini' to transcribe with transcriber, or 'cancel' to abort
|
|
212
233
|
"""
|
|
213
234
|
if not files:
|
|
214
|
-
return
|
|
235
|
+
return "proceed"
|
|
215
236
|
|
|
216
237
|
# If only one file, simplify the choice
|
|
217
238
|
if len(files) == 1:
|
|
@@ -220,7 +241,7 @@ class FileExistenceManager:
|
|
|
220
241
|
file_type=file_type, files=files, operation=operation
|
|
221
242
|
)
|
|
222
243
|
if files
|
|
223
|
-
else
|
|
244
|
+
else "proceed"
|
|
224
245
|
)
|
|
225
246
|
|
|
226
247
|
# Multiple files: let user choose which one
|
|
@@ -228,27 +249,30 @@ class FileExistenceManager:
|
|
|
228
249
|
|
|
229
250
|
# Create options with full file paths
|
|
230
251
|
options = []
|
|
231
|
-
for i, file_path in enumerate(files, 1):
|
|
252
|
+
for i, file_path in enumerate(sorted(files), 1):
|
|
232
253
|
# Display full path for clarity
|
|
233
|
-
options.append((f
|
|
254
|
+
options.append((f"{colorful.cyan(file_path)}", file_path))
|
|
234
255
|
|
|
235
|
-
# Add
|
|
236
|
-
if
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
256
|
+
# Add transcription or overwrite option
|
|
257
|
+
if transcriber_name:
|
|
258
|
+
transcribe_text = f"✨ Transcribe with {transcriber_name}"
|
|
259
|
+
options.append((colorful.magenta(transcribe_text), TRANSCRIBE_CHOICE))
|
|
260
|
+
else:
|
|
261
|
+
overwrite_text = f"Overwrite (re-{operation} or download)"
|
|
262
|
+
options.append((colorful.yellow(overwrite_text), "overwrite"))
|
|
263
|
+
options.append((colorful.red("Cancel operation"), "cancel"))
|
|
242
264
|
|
|
243
|
-
prompt_message = colorful.bold_black_on_cyan(f
|
|
265
|
+
prompt_message = colorful.bold_black_on_cyan(f"Select which {file_type} to use:")
|
|
244
266
|
choice = FileExistenceManager._prompt_user_choice(prompt_message, options, default=files[0])
|
|
245
267
|
|
|
246
|
-
if choice ==
|
|
268
|
+
if choice == "cancel":
|
|
247
269
|
print(f'{colorful.red("❌ Operation cancelled")}')
|
|
248
|
-
elif choice ==
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
270
|
+
elif choice == "overwrite":
|
|
271
|
+
overwrite_msg = f"🔄 Overwriting all {file_type} files"
|
|
272
|
+
print(f"{colorful.yellow(overwrite_msg)}")
|
|
273
|
+
elif choice == TRANSCRIBE_CHOICE:
|
|
274
|
+
transcribe_msg = f"✨ Will transcribe with {transcriber_name}"
|
|
275
|
+
print(f"{colorful.magenta(transcribe_msg)}")
|
|
252
276
|
else:
|
|
253
277
|
print(f'{colorful.green(f"✅ Using: {choice}")}')
|
|
254
278
|
|
|
@@ -256,7 +280,7 @@ class FileExistenceManager:
|
|
|
256
280
|
|
|
257
281
|
@staticmethod
|
|
258
282
|
def prompt_per_file_type_confirmation(
|
|
259
|
-
existing_files: Dict[str, List[str]], operation: str =
|
|
283
|
+
existing_files: Dict[str, List[str]], operation: str = "download"
|
|
260
284
|
) -> Dict[str, str]:
|
|
261
285
|
"""
|
|
262
286
|
Prompt user for confirmation for each file type, combining interactive selections when possible.
|
|
@@ -269,7 +293,7 @@ class FileExistenceManager:
|
|
|
269
293
|
Dictionary mapping file type to user choice ('use', 'overwrite', 'proceed', or 'cancel')
|
|
270
294
|
"""
|
|
271
295
|
ordered_types = []
|
|
272
|
-
for preferred in [
|
|
296
|
+
for preferred in ["media", "audio", "video", "caption"]:
|
|
273
297
|
if preferred not in ordered_types:
|
|
274
298
|
ordered_types.append(preferred)
|
|
275
299
|
for file_type in existing_files.keys():
|
|
@@ -277,7 +301,7 @@ class FileExistenceManager:
|
|
|
277
301
|
ordered_types.append(file_type)
|
|
278
302
|
|
|
279
303
|
file_types_with_files = [ft for ft in ordered_types if existing_files.get(ft)]
|
|
280
|
-
choices = {ft:
|
|
304
|
+
choices = {ft: "proceed" for ft in ordered_types}
|
|
281
305
|
|
|
282
306
|
if not file_types_with_files:
|
|
283
307
|
return choices
|
|
@@ -292,9 +316,9 @@ class FileExistenceManager:
|
|
|
292
316
|
for file_type in file_types_with_files:
|
|
293
317
|
choice = FileExistenceManager.prompt_file_type_confirmation(file_type, existing_files[file_type], operation)
|
|
294
318
|
choices[file_type] = choice
|
|
295
|
-
if choice ==
|
|
319
|
+
if choice == "cancel":
|
|
296
320
|
for remaining in file_types_with_files[file_types_with_files.index(file_type) + 1 :]:
|
|
297
|
-
choices[remaining] =
|
|
321
|
+
choices[remaining] = "cancel"
|
|
298
322
|
break
|
|
299
323
|
|
|
300
324
|
return choices
|
|
@@ -339,7 +363,7 @@ class FileExistenceManager:
|
|
|
339
363
|
default=default,
|
|
340
364
|
).ask()
|
|
341
365
|
except (KeyboardInterrupt, EOFError):
|
|
342
|
-
return
|
|
366
|
+
return "cancel"
|
|
343
367
|
except Exception:
|
|
344
368
|
selection = None
|
|
345
369
|
|
|
@@ -347,7 +371,7 @@ class FileExistenceManager:
|
|
|
347
371
|
return selection
|
|
348
372
|
if default:
|
|
349
373
|
return default
|
|
350
|
-
return
|
|
374
|
+
return "cancel"
|
|
351
375
|
|
|
352
376
|
return FileExistenceManager._prompt_with_numeric_input(prompt_message, options, default)
|
|
353
377
|
|
|
@@ -358,17 +382,17 @@ class FileExistenceManager:
|
|
|
358
382
|
default: str = None,
|
|
359
383
|
) -> str:
|
|
360
384
|
numbered_choices = {str(index + 1): value for index, (_, value) in enumerate(options)}
|
|
361
|
-
label_lines = [f
|
|
362
|
-
prompt_header = f
|
|
385
|
+
label_lines = [f"{index + 1}. {label}" for index, (label, _) in enumerate(options)]
|
|
386
|
+
prompt_header = f"{prompt_message}"
|
|
363
387
|
print(prompt_header)
|
|
364
388
|
for line in label_lines:
|
|
365
389
|
print(line)
|
|
366
390
|
|
|
367
391
|
while True:
|
|
368
392
|
try:
|
|
369
|
-
raw_choice = input(f
|
|
393
|
+
raw_choice = input(f"\nEnter your choice (1-{len(options)}): ").strip()
|
|
370
394
|
except (EOFError, KeyboardInterrupt):
|
|
371
|
-
return
|
|
395
|
+
return "cancel"
|
|
372
396
|
|
|
373
397
|
if not raw_choice and default:
|
|
374
398
|
return default
|
|
@@ -376,7 +400,7 @@ class FileExistenceManager:
|
|
|
376
400
|
if raw_choice in numbered_choices:
|
|
377
401
|
return numbered_choices[raw_choice]
|
|
378
402
|
|
|
379
|
-
print(
|
|
403
|
+
print("Invalid choice. Please enter one of the displayed numbers.")
|
|
380
404
|
|
|
381
405
|
@staticmethod
|
|
382
406
|
def _combined_file_type_prompt(
|
|
@@ -404,7 +428,7 @@ class FileExistenceManager:
|
|
|
404
428
|
file_types: Sequence[str],
|
|
405
429
|
operation: str,
|
|
406
430
|
) -> Optional[Dict[str, str]]:
|
|
407
|
-
states = {file_type:
|
|
431
|
+
states = {file_type: "use" for file_type in file_types}
|
|
408
432
|
selected_index = 0
|
|
409
433
|
total_items = len(file_types) + 2 # file types + confirm + cancel
|
|
410
434
|
total_lines = len(file_types) + 4 # prompt + items + confirm/cancel + instructions
|
|
@@ -414,7 +438,7 @@ class FileExistenceManager:
|
|
|
414
438
|
num_mapping = {str(index + 1): index for index in range(len(file_types))}
|
|
415
439
|
|
|
416
440
|
try:
|
|
417
|
-
if os.name ==
|
|
441
|
+
if os.name == "nt":
|
|
418
442
|
read_key = FileExistenceManager._read_key_windows
|
|
419
443
|
raw_mode = _NullContext()
|
|
420
444
|
else:
|
|
@@ -424,20 +448,20 @@ class FileExistenceManager:
|
|
|
424
448
|
with raw_mode:
|
|
425
449
|
while True:
|
|
426
450
|
key = read_key()
|
|
427
|
-
if key ==
|
|
451
|
+
if key == "up":
|
|
428
452
|
selected_index = (selected_index - 1) % total_items
|
|
429
453
|
FileExistenceManager._refresh_combined_file_menu(
|
|
430
454
|
total_lines, existing_files, file_types, states, selected_index, operation
|
|
431
455
|
)
|
|
432
|
-
elif key ==
|
|
456
|
+
elif key == "down":
|
|
433
457
|
selected_index = (selected_index + 1) % total_items
|
|
434
458
|
FileExistenceManager._refresh_combined_file_menu(
|
|
435
459
|
total_lines, existing_files, file_types, states, selected_index, operation
|
|
436
460
|
)
|
|
437
|
-
elif key in (
|
|
461
|
+
elif key in ("enter", "space"):
|
|
438
462
|
if selected_index < len(file_types):
|
|
439
463
|
current_type = file_types[selected_index]
|
|
440
|
-
states[current_type] =
|
|
464
|
+
states[current_type] = "overwrite" if states[current_type] == "use" else "use"
|
|
441
465
|
FileExistenceManager._refresh_combined_file_menu(
|
|
442
466
|
total_lines, existing_files, file_types, states, selected_index, operation
|
|
443
467
|
)
|
|
@@ -445,7 +469,7 @@ class FileExistenceManager:
|
|
|
445
469
|
return FileExistenceManager._finalize_combined_states(file_types, states)
|
|
446
470
|
else:
|
|
447
471
|
return FileExistenceManager._cancel_combined_states(file_types)
|
|
448
|
-
elif key ==
|
|
472
|
+
elif key == "cancel":
|
|
449
473
|
return FileExistenceManager._cancel_combined_states(file_types)
|
|
450
474
|
elif key in num_mapping:
|
|
451
475
|
selected_index = num_mapping[key]
|
|
@@ -469,49 +493,49 @@ class FileExistenceManager:
|
|
|
469
493
|
selected_index: int,
|
|
470
494
|
operation: str,
|
|
471
495
|
) -> None:
|
|
472
|
-
prompt = colorful.bold_black_on_cyan(
|
|
496
|
+
prompt = colorful.bold_black_on_cyan("Select how to handle existing files")
|
|
473
497
|
print(prompt)
|
|
474
498
|
|
|
475
499
|
for idx, file_type in enumerate(file_types):
|
|
476
|
-
label = FileExistenceManager.FILE_TYPE_INFO.get(file_type, (
|
|
500
|
+
label = FileExistenceManager.FILE_TYPE_INFO.get(file_type, ("📄", file_type.capitalize()))[1]
|
|
477
501
|
count = len(existing_files.get(file_type, []))
|
|
478
502
|
count_suffix = f' ({count} file{"s" if count != 1 else ""})'
|
|
479
|
-
state_plain =
|
|
503
|
+
state_plain = "Use existing" if states[file_type] == "use" else f"Overwrite ({operation})"
|
|
480
504
|
if idx == selected_index:
|
|
481
|
-
prefix = colorful.bold_white(
|
|
482
|
-
line = colorful.bold_black_on_cyan(f
|
|
505
|
+
prefix = colorful.bold_white(">")
|
|
506
|
+
line = colorful.bold_black_on_cyan(f"{label}: {state_plain}{count_suffix}")
|
|
483
507
|
else:
|
|
484
|
-
prefix =
|
|
485
|
-
if states[file_type] ==
|
|
486
|
-
state_text = colorful.green(
|
|
508
|
+
prefix = " "
|
|
509
|
+
if states[file_type] == "use":
|
|
510
|
+
state_text = colorful.green("Use existing")
|
|
487
511
|
else:
|
|
488
|
-
state_text = colorful.yellow(f
|
|
489
|
-
line = f
|
|
490
|
-
print(f
|
|
512
|
+
state_text = colorful.yellow(f"Overwrite ({operation})")
|
|
513
|
+
line = f"{label}: {state_text}{count_suffix}"
|
|
514
|
+
print(f"{prefix} {line}")
|
|
491
515
|
|
|
492
516
|
confirm_index = len(file_types)
|
|
493
517
|
cancel_index = len(file_types) + 1
|
|
494
518
|
|
|
495
519
|
if selected_index == confirm_index:
|
|
496
|
-
confirm_line = colorful.bold_black_on_cyan(
|
|
497
|
-
confirm_prefix = colorful.bold_white(
|
|
520
|
+
confirm_line = colorful.bold_black_on_cyan("Confirm selections")
|
|
521
|
+
confirm_prefix = colorful.bold_white(">")
|
|
498
522
|
else:
|
|
499
|
-
confirm_line = colorful.bold_green(
|
|
500
|
-
confirm_prefix =
|
|
501
|
-
print(f
|
|
523
|
+
confirm_line = colorful.bold_green("Confirm selections")
|
|
524
|
+
confirm_prefix = " "
|
|
525
|
+
print(f"{confirm_prefix} {confirm_line}")
|
|
502
526
|
|
|
503
527
|
if selected_index == cancel_index:
|
|
504
|
-
cancel_line = colorful.bold_black_on_cyan(
|
|
505
|
-
cancel_prefix = colorful.bold_white(
|
|
528
|
+
cancel_line = colorful.bold_black_on_cyan("Cancel operation")
|
|
529
|
+
cancel_prefix = colorful.bold_white(">")
|
|
506
530
|
else:
|
|
507
|
-
cancel_line = colorful.bold_red(
|
|
508
|
-
cancel_prefix =
|
|
509
|
-
print(f
|
|
531
|
+
cancel_line = colorful.bold_red("Cancel operation")
|
|
532
|
+
cancel_prefix = " "
|
|
533
|
+
print(f"{cancel_prefix} {cancel_line}")
|
|
510
534
|
|
|
511
535
|
print(
|
|
512
|
-
|
|
513
|
-
+ colorful.bold_black_on_cyan(
|
|
514
|
-
+
|
|
536
|
+
"Use "
|
|
537
|
+
+ colorful.bold_black_on_cyan("↑/↓")
|
|
538
|
+
+ " to navigate. Enter/Space toggles an item. Confirm to proceed or cancel to abort."
|
|
515
539
|
)
|
|
516
540
|
|
|
517
541
|
@staticmethod
|
|
@@ -523,58 +547,58 @@ class FileExistenceManager:
|
|
|
523
547
|
selected_index: int,
|
|
524
548
|
operation: str,
|
|
525
549
|
) -> None:
|
|
526
|
-
move_up =
|
|
527
|
-
clear_line =
|
|
550
|
+
move_up = "\033[F" * total_lines
|
|
551
|
+
clear_line = "\033[K"
|
|
528
552
|
sys.stdout.write(move_up)
|
|
529
553
|
sys.stdout.write(clear_line)
|
|
530
554
|
sys.stdout.flush()
|
|
531
555
|
|
|
532
|
-
prompt = colorful.bold_black_on_cyan(
|
|
556
|
+
prompt = colorful.bold_black_on_cyan("Select how to handle existing files")
|
|
533
557
|
print(prompt)
|
|
534
558
|
|
|
535
559
|
for idx, file_type in enumerate(file_types):
|
|
536
560
|
sys.stdout.write(clear_line)
|
|
537
|
-
label = FileExistenceManager.FILE_TYPE_INFO.get(file_type, (
|
|
561
|
+
label = FileExistenceManager.FILE_TYPE_INFO.get(file_type, ("📄", file_type.capitalize()))[1]
|
|
538
562
|
count = len(existing_files.get(file_type, []))
|
|
539
563
|
count_suffix = f' ({count} file{"s" if count != 1 else ""})'
|
|
540
|
-
state_plain =
|
|
564
|
+
state_plain = "Use existing" if states[file_type] == "use" else f"Overwrite ({operation})"
|
|
541
565
|
if idx == selected_index:
|
|
542
|
-
prefix = colorful.bold_white(
|
|
543
|
-
line = colorful.bold_black_on_cyan(f
|
|
566
|
+
prefix = colorful.bold_white(">")
|
|
567
|
+
line = colorful.bold_black_on_cyan(f"{label}: {state_plain}{count_suffix}")
|
|
544
568
|
else:
|
|
545
|
-
prefix =
|
|
546
|
-
if states[file_type] ==
|
|
547
|
-
state_text = colorful.green(
|
|
569
|
+
prefix = " "
|
|
570
|
+
if states[file_type] == "use":
|
|
571
|
+
state_text = colorful.green("Use existing")
|
|
548
572
|
else:
|
|
549
|
-
state_text = colorful.yellow(f
|
|
550
|
-
line = f
|
|
551
|
-
print(f
|
|
573
|
+
state_text = colorful.yellow(f"Overwrite ({operation})")
|
|
574
|
+
line = f"{label}: {state_text}{count_suffix}"
|
|
575
|
+
print(f"{prefix} {line}")
|
|
552
576
|
|
|
553
577
|
sys.stdout.write(clear_line)
|
|
554
578
|
confirm_index = len(file_types)
|
|
555
579
|
cancel_index = len(file_types) + 1
|
|
556
580
|
if selected_index == confirm_index:
|
|
557
|
-
confirm_line = colorful.bold_black_on_cyan(
|
|
558
|
-
confirm_prefix = colorful.bold_white(
|
|
581
|
+
confirm_line = colorful.bold_black_on_cyan("Confirm selections")
|
|
582
|
+
confirm_prefix = colorful.bold_white(">")
|
|
559
583
|
else:
|
|
560
|
-
confirm_line = colorful.bold_green(
|
|
561
|
-
confirm_prefix =
|
|
562
|
-
print(f
|
|
584
|
+
confirm_line = colorful.bold_green("Confirm selections")
|
|
585
|
+
confirm_prefix = " "
|
|
586
|
+
print(f"{confirm_prefix} {confirm_line}")
|
|
563
587
|
|
|
564
588
|
sys.stdout.write(clear_line)
|
|
565
589
|
if selected_index == cancel_index:
|
|
566
|
-
cancel_line = colorful.bold_black_on_cyan(
|
|
567
|
-
cancel_prefix = colorful.bold_white(
|
|
590
|
+
cancel_line = colorful.bold_black_on_cyan("Cancel operation")
|
|
591
|
+
cancel_prefix = colorful.bold_white(">")
|
|
568
592
|
else:
|
|
569
|
-
cancel_line = colorful.bold_red(
|
|
570
|
-
cancel_prefix =
|
|
571
|
-
print(f
|
|
593
|
+
cancel_line = colorful.bold_red("Cancel operation")
|
|
594
|
+
cancel_prefix = " "
|
|
595
|
+
print(f"{cancel_prefix} {cancel_line}")
|
|
572
596
|
|
|
573
597
|
sys.stdout.write(clear_line)
|
|
574
598
|
print(
|
|
575
|
-
|
|
576
|
-
+ colorful.bold_black_on_cyan(
|
|
577
|
-
+
|
|
599
|
+
"Use "
|
|
600
|
+
+ colorful.bold_black_on_cyan("↑/↓")
|
|
601
|
+
+ " to navigate. Enter/Space toggles an item. Confirm to proceed or cancel to abort."
|
|
578
602
|
)
|
|
579
603
|
sys.stdout.flush()
|
|
580
604
|
|
|
@@ -586,41 +610,41 @@ class FileExistenceManager:
|
|
|
586
610
|
) -> Dict[str, str]:
|
|
587
611
|
label_choices = []
|
|
588
612
|
for file_type in file_types:
|
|
589
|
-
label = FileExistenceManager.FILE_TYPE_INFO.get(file_type, (
|
|
613
|
+
label = FileExistenceManager.FILE_TYPE_INFO.get(file_type, ("📄", file_type.capitalize()))[1]
|
|
590
614
|
count = len(existing_files.get(file_type, []))
|
|
591
615
|
count_suffix = f' ({count} file{"s" if count != 1 else ""})'
|
|
592
|
-
label_choices.append(questionary.Choice(title=f
|
|
616
|
+
label_choices.append(questionary.Choice(title=f"{label}{count_suffix}", value=file_type))
|
|
593
617
|
|
|
594
|
-
label_choices.append(questionary.Choice(title=
|
|
618
|
+
label_choices.append(questionary.Choice(title="Cancel operation", value="__cancel__"))
|
|
595
619
|
|
|
596
620
|
try:
|
|
597
621
|
selection = questionary.checkbox(
|
|
598
|
-
message=
|
|
622
|
+
message="Select file types to overwrite (others will use existing files)",
|
|
599
623
|
choices=label_choices,
|
|
600
|
-
instruction=
|
|
624
|
+
instruction="Press Space to toggle overwrite. Press Enter to confirm.",
|
|
601
625
|
).ask()
|
|
602
626
|
except (KeyboardInterrupt, EOFError):
|
|
603
627
|
return FileExistenceManager._cancel_combined_states(file_types)
|
|
604
628
|
|
|
605
|
-
if selection is None or
|
|
629
|
+
if selection is None or "__cancel__" in selection:
|
|
606
630
|
return FileExistenceManager._cancel_combined_states(file_types)
|
|
607
631
|
|
|
608
|
-
states = {file_type: (
|
|
632
|
+
states = {file_type: ("overwrite" if file_type in selection else "use") for file_type in file_types}
|
|
609
633
|
return FileExistenceManager._finalize_combined_states(file_types, states)
|
|
610
634
|
|
|
611
635
|
@staticmethod
|
|
612
636
|
def _finalize_combined_states(file_types: Sequence[str], states: Dict[str, str]) -> Dict[str, str]:
|
|
613
|
-
return {file_type: (
|
|
637
|
+
return {file_type: ("overwrite" if states.get(file_type) == "overwrite" else "use") for file_type in file_types}
|
|
614
638
|
|
|
615
639
|
@staticmethod
|
|
616
640
|
def _cancel_combined_states(file_types: Sequence[str]) -> Dict[str, str]:
|
|
617
|
-
return {file_type:
|
|
641
|
+
return {file_type: "cancel" for file_type in file_types}
|
|
618
642
|
|
|
619
643
|
@staticmethod
|
|
620
644
|
def _supports_native_selector() -> bool:
|
|
621
645
|
if not sys.stdin.isatty() or not sys.stdout.isatty():
|
|
622
646
|
return False
|
|
623
|
-
if os.name ==
|
|
647
|
+
if os.name == "nt":
|
|
624
648
|
try:
|
|
625
649
|
import msvcrt # noqa: F401
|
|
626
650
|
except ImportError:
|
|
@@ -653,7 +677,7 @@ class FileExistenceManager:
|
|
|
653
677
|
FileExistenceManager._render_menu(prompt_message, options, selected_index)
|
|
654
678
|
|
|
655
679
|
try:
|
|
656
|
-
if os.name ==
|
|
680
|
+
if os.name == "nt":
|
|
657
681
|
read_key = FileExistenceManager._read_key_windows
|
|
658
682
|
raw_mode = _NullContext()
|
|
659
683
|
else:
|
|
@@ -663,20 +687,20 @@ class FileExistenceManager:
|
|
|
663
687
|
with raw_mode:
|
|
664
688
|
while True:
|
|
665
689
|
key = read_key()
|
|
666
|
-
if key ==
|
|
690
|
+
if key == "up":
|
|
667
691
|
selected_index = (selected_index - 1) % len(options)
|
|
668
692
|
FileExistenceManager._refresh_menu(total_lines, prompt_message, options, selected_index)
|
|
669
|
-
elif key ==
|
|
693
|
+
elif key == "down":
|
|
670
694
|
selected_index = (selected_index + 1) % len(options)
|
|
671
695
|
FileExistenceManager._refresh_menu(total_lines, prompt_message, options, selected_index)
|
|
672
|
-
elif key ==
|
|
696
|
+
elif key == "enter":
|
|
673
697
|
return options[selected_index][1]
|
|
674
698
|
elif key in value_by_number:
|
|
675
699
|
return value_by_number[key]
|
|
676
|
-
elif key ==
|
|
677
|
-
return
|
|
700
|
+
elif key == "cancel":
|
|
701
|
+
return "cancel"
|
|
678
702
|
except KeyboardInterrupt:
|
|
679
|
-
return
|
|
703
|
+
return "cancel"
|
|
680
704
|
except Exception:
|
|
681
705
|
return FileExistenceManager._prompt_with_numeric_input(prompt_message, options, default)
|
|
682
706
|
finally:
|
|
@@ -684,17 +708,17 @@ class FileExistenceManager:
|
|
|
684
708
|
|
|
685
709
|
@staticmethod
|
|
686
710
|
def _render_menu(prompt_message: str, options: Sequence[Tuple[str, str]], selected_index: int) -> None:
|
|
687
|
-
prompt = f
|
|
711
|
+
prompt = f"{prompt_message}"
|
|
688
712
|
print(prompt)
|
|
689
713
|
for idx, (label, _) in enumerate(options):
|
|
690
714
|
if idx == selected_index:
|
|
691
|
-
prefix = colorful.bold_white(
|
|
715
|
+
prefix = colorful.bold_white(">")
|
|
692
716
|
suffix = colorful.bold_black_on_cyan(str(label))
|
|
693
717
|
else:
|
|
694
|
-
prefix =
|
|
718
|
+
prefix = " "
|
|
695
719
|
suffix = label
|
|
696
|
-
print(f
|
|
697
|
-
print(
|
|
720
|
+
print(f"{prefix} {suffix}")
|
|
721
|
+
print("Use " + colorful.bold_black_on_cyan("↑/↓") + " to move, Enter to confirm, or press a number to choose.")
|
|
698
722
|
|
|
699
723
|
@staticmethod
|
|
700
724
|
def _refresh_menu(
|
|
@@ -703,24 +727,24 @@ class FileExistenceManager:
|
|
|
703
727
|
options: Sequence[Tuple[str, str]],
|
|
704
728
|
selected_index: int,
|
|
705
729
|
) -> None:
|
|
706
|
-
move_up =
|
|
707
|
-
clear_line =
|
|
730
|
+
move_up = "\033[F" * total_lines
|
|
731
|
+
clear_line = "\033[K"
|
|
708
732
|
sys.stdout.write(move_up)
|
|
709
733
|
sys.stdout.write(clear_line)
|
|
710
734
|
sys.stdout.flush()
|
|
711
|
-
prompt = f
|
|
735
|
+
prompt = f"{prompt_message}"
|
|
712
736
|
print(prompt)
|
|
713
737
|
for idx, (label, _) in enumerate(options):
|
|
714
738
|
sys.stdout.write(clear_line)
|
|
715
739
|
if idx == selected_index:
|
|
716
|
-
prefix = colorful.bold_white(
|
|
740
|
+
prefix = colorful.bold_white(">")
|
|
717
741
|
suffix = colorful.bold_black_on_cyan(str(label))
|
|
718
742
|
else:
|
|
719
|
-
prefix =
|
|
743
|
+
prefix = " "
|
|
720
744
|
suffix = label
|
|
721
|
-
print(f
|
|
745
|
+
print(f"{prefix} {suffix}")
|
|
722
746
|
sys.stdout.write(clear_line)
|
|
723
|
-
print(
|
|
747
|
+
print("Use " + colorful.bold_black_on_cyan("↑/↓") + " to move, Enter to confirm, or press a number to choose.")
|
|
724
748
|
sys.stdout.flush()
|
|
725
749
|
|
|
726
750
|
@staticmethod
|
|
@@ -729,53 +753,53 @@ class FileExistenceManager:
|
|
|
729
753
|
|
|
730
754
|
while True:
|
|
731
755
|
ch = msvcrt.getwch()
|
|
732
|
-
if ch in (
|
|
756
|
+
if ch in ("\x00", "\xe0"):
|
|
733
757
|
extended = msvcrt.getwch()
|
|
734
|
-
if extended ==
|
|
735
|
-
return
|
|
736
|
-
if extended ==
|
|
737
|
-
return
|
|
758
|
+
if extended == "H":
|
|
759
|
+
return "up"
|
|
760
|
+
if extended == "P":
|
|
761
|
+
return "down"
|
|
738
762
|
continue
|
|
739
|
-
if ch in (
|
|
740
|
-
return
|
|
741
|
-
if ch ==
|
|
742
|
-
return
|
|
763
|
+
if ch in ("\r", "\n"):
|
|
764
|
+
return "enter"
|
|
765
|
+
if ch == " ":
|
|
766
|
+
return "space"
|
|
743
767
|
if ch.isdigit():
|
|
744
768
|
return ch
|
|
745
|
-
if ch.lower() in (
|
|
746
|
-
return
|
|
747
|
-
if ch ==
|
|
748
|
-
return
|
|
749
|
-
if ch ==
|
|
769
|
+
if ch.lower() in ("j", "k"):
|
|
770
|
+
return "down" if ch.lower() == "j" else "up"
|
|
771
|
+
if ch == "\x1b":
|
|
772
|
+
return "cancel"
|
|
773
|
+
if ch == "\x03":
|
|
750
774
|
raise KeyboardInterrupt
|
|
751
775
|
|
|
752
776
|
@staticmethod
|
|
753
777
|
def _read_key_posix() -> str:
|
|
754
778
|
ch = sys.stdin.read(1)
|
|
755
|
-
if ch ==
|
|
779
|
+
if ch == "\x1b":
|
|
756
780
|
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
|
|
781
|
+
if seq == "[A":
|
|
782
|
+
return "up"
|
|
783
|
+
if seq == "[B":
|
|
784
|
+
return "down"
|
|
785
|
+
return "cancel"
|
|
786
|
+
if ch in ("\r", "\n"):
|
|
787
|
+
return "enter"
|
|
788
|
+
if ch == " ":
|
|
789
|
+
return "space"
|
|
766
790
|
if ch.isdigit():
|
|
767
791
|
return ch
|
|
768
|
-
if ch.lower() ==
|
|
769
|
-
return
|
|
770
|
-
if ch.lower() ==
|
|
771
|
-
return
|
|
772
|
-
if ch ==
|
|
792
|
+
if ch.lower() == "j":
|
|
793
|
+
return "down"
|
|
794
|
+
if ch.lower() == "k":
|
|
795
|
+
return "up"
|
|
796
|
+
if ch == "\x03":
|
|
773
797
|
raise KeyboardInterrupt
|
|
774
|
-
return
|
|
798
|
+
return ""
|
|
775
799
|
|
|
776
800
|
@staticmethod
|
|
777
801
|
def _stdin_raw_mode():
|
|
778
|
-
if os.name ==
|
|
802
|
+
if os.name == "nt":
|
|
779
803
|
return _NullContext()
|
|
780
804
|
|
|
781
805
|
import termios
|