git4subeditor 0.1.20__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.
@@ -0,0 +1,137 @@
1
+ <?xml version='1.0' encoding='utf-8'?>
2
+ <locale language="en">
3
+ <message key="app.description">Convert Barotrauma .sub files to and from Git-friendly XML bundles.</message>
4
+ <message key="metavar.command">command</message>
5
+ <message key="arg.version">show program's version number and exit</message>
6
+ <message key="arg.help">show this help message and exit</message>
7
+ <message key="help.usage">usage: </message>
8
+ <message key="help.positionals">positional arguments</message>
9
+ <message key="help.options">options</message>
10
+ <message key="repl.commands">Commands</message>
11
+ <message key="repl.command.init">initialize/update workspace</message>
12
+ <message key="repl.command.export">extract .sub and convert to Git-friendly XML</message>
13
+ <message key="repl.command.pack">pack XML into .sub</message>
14
+ <message key="repl.command.unpack">extract .sub as raw XML only</message>
15
+ <message key="repl.command.tree">recursively search and show all submarines in the repository</message>
16
+ <message key="repl.command.exit">quit interactive mode</message>
17
+ <message key="repl.hint">Tab complete · ↑↓ history · Ctrl-R search · Ctrl-D quit · ? help</message>
18
+ <message key="repl.help"><![CDATA[<title>tree</title>
19
+ Show submarine .sub files in the Git repository as a directory tree.
20
+ - Unregistered files end with a red (U)
21
+ - Registered files with a changed hash end with a yellow (M)
22
+
23
+ <title>export</title>
24
+ Export .sub files as Git-friendly XML.
25
+
26
+ <cmd>export</cmd>
27
+ Export all changed registered submarines
28
+ <cmd>export</cmd> "test/My Sub1.sub" "test/My Sub2.sub" ...
29
+ Export one or more registered submarines
30
+ <cmd>export</cmd> "test/My Sub1.sub" -o ./mydir
31
+ Export a submarine to the specified directory
32
+
33
+ Note: if a submarine or directory name contains spaces, quote the path;
34
+ otherwise it will be parsed as multiple arguments.
35
+
36
+ <title>pack</title>
37
+ Pack exported XML back into .sub.
38
+
39
+ <cmd>pack</cmd> "test/My Sub.xml"
40
+ <cmd>pack</cmd> "test/My Sub.xml" -o ./mydir
41
+ Pack into the specified directory
42
+
43
+ Note: if the output .sub already exists, it is backed up to .sub.bak first,
44
+ then overwritten.
45
+
46
+ <title>unpack</title>
47
+ Extract a submarine .sub file as raw XML. This command is rarely used.
48
+
49
+ <cmd>unpack</cmd> "test/My Sub.sub"
50
+ ]]></message>
51
+ <message key="repl.not_tty">interactive mode requires a terminal; use a subcommand instead</message>
52
+ <message key="repl.parse_error">cannot parse command line: {error}</message>
53
+ <message key="repl.workspace_missing">workspace not initialized</message>
54
+ <message key="repl.workspace">Project: {project} | Git repo: {git_repo} | Mod root: {mod_root}</message>
55
+ <message key="repl.cwd">CWD: {path}</message>
56
+
57
+ <message key="command.init">initialize/update a workspace</message>
58
+ <message key="command.unpack">extract raw XML from .sub files</message>
59
+ <message key="command.export">export a .sub file as canonical XML plus a preview image</message>
60
+ <message key="command.pack">pack an exported XML file back into a .sub file</message>
61
+ <message key="command.sub">list registered submarines</message>
62
+
63
+ <message key="arg.inputs.sub">one or more .sub files</message>
64
+ <message key="arg.inputs.xml">one or more exported XML files</message>
65
+ <message key="arg.output_dir">output directory, defaults to each input file's directory</message>
66
+ <message key="arg.encoding">force an XML encoding instead of auto-detecting UTF-8/UTF-16</message>
67
+ <message key="arg.stdout">print XML to stdout instead of writing a file</message>
68
+ <message key="arg.no_validate">skip the XML well-formedness check</message>
69
+ <message key="arg.force">overwrite existing output files without prompting</message>
70
+ <message key="arg.sort_attributes">sort XML attributes alphabetically for more stable diffs</message>
71
+ <message key="arg.sort_attributes_pack">sort XML attributes alphabetically before packing</message>
72
+ <message key="arg.output">output .sub file, or an existing directory; defaults to the XML file's directory</message>
73
+ <message key="arg.tree">show registered submarines as a directory tree</message>
74
+ <message key="arg.force_init">accept all initialization changes without prompting</message>
75
+
76
+ <message key="err.stdout_single">--stdout accepts only one input file</message>
77
+ <message key="err.output_multi">--output must be an existing directory when packing multiple files</message>
78
+ <message key="prompt.overwrite">{path} already exists. Overwrite? [y/N] </message>
79
+ <message key="prompt.submarines">Update these submarine entries? [y/N] </message>
80
+ <message key="prompt.remove_submarines">Remove these submarine entries? [y/N] </message>
81
+ <message key="prompt.mod_root_changed">MOD root: {old} -> {new}. Update? [y/N] </message>
82
+ <message key="prompt.git_repo_changed">Git repo: {old} -> {new}. Update? [y/N] </message>
83
+ <message key="prompt.export_changed">Export these submarines? [y/N] </message>
84
+ <message key="status.skipped">skipped: {path}</message>
85
+ <message key="status.no_changed_submarines">no changed registered submarines</message>
86
+ <message key="status.export_changed_header">Registered submarines with changes:</message>
87
+ <message key="status.export_declined">export cancelled</message>
88
+ <message key="status.export_preview">{src} -> {xml}, preview: {preview}</message>
89
+ <message key="status.export_no_preview">{src} -> {xml}, no preview</message>
90
+ <message key="status.pack">{src} -> {dst}</message>
91
+ <message key="status.unpack">{src} -> {dst} [{kind}, {bytes} bytes]</message>
92
+ <message key="kind.gzip">gzip</message>
93
+ <message key="kind.plain_xml">plain XML</message>
94
+ <message key="status.git_repo">Git repo: {path}</message>
95
+
96
+ <message key="warning.current_not_repo">current directory is not a Git repo; using {path}</message>
97
+ <message key="warning.no_repo">no Git repo found up to the MOD root; run 'git init' to initialize one</message>
98
+ <message key="warning.upper_repo">this project also has an upper-level Git repo: {path}</message>
99
+ <message key="warning.preview_missing">preview image not found: {path}; preview will be left empty</message>
100
+ <message key="warning.registry_preview_failed">failed to read preview for submarine {name}; using the default preview</message>
101
+ <message key="warning.decode_replace">input is not valid UTF-8/UTF-16, decoding as UTF-8 with replacement characters</message>
102
+
103
+ <message key="init.created_gitignore">created {name}; added {patterns}</message>
104
+ <message key="init.updated_gitignore">updated {name}; added {patterns}</message>
105
+ <message key="init.gitignore_ok">{name} already ignores {patterns}</message>
106
+ <message key="init.config_created">created {name}</message>
107
+ <message key="init.config_updated">updated {name}</message>
108
+ <message key="init.config_exists">{name} already exists</message>
109
+ <message key="init.registered_header">Registered submarines:</message>
110
+ <message key="init.pending_header">Submarines to add or update:</message>
111
+ <message key="init.removed_header">Registered submarines to remove:</message>
112
+ <message key="init.registered">registered {count} submarine files</message>
113
+ <message key="init.submarines_skipped">submarine registry not updated</message>
114
+ <message key="init.language_set">Language set to English</message>
115
+ <message key="err.init">cannot initialize project: {error}</message>
116
+ <message key="err.not_mod">not a Barotrauma MOD project: filelist.xml not found in {path} or its parent directories</message>
117
+ <message key="err.invalid_config">invalid project configuration {path}: {error}</message>
118
+ <message key="err.not_registered">submarine is not registered in git4subeditor.xml: {path}</message>
119
+ <message key="err.invalid_xml">invalid XML: {error}</message>
120
+ <message key="err.invalid_base64">previewimage is not valid base64: {error}</message>
121
+ <message key="err.read_file">cannot read file {path}: {error}</message>
122
+ <message key="err.pack_sub_input">{path} looks like a .sub file; export it to XML first</message>
123
+ <message key="err.read_preview">cannot read preview image {path}: {error}</message>
124
+ <message key="err.empty_preview">preview image is empty: {path}</message>
125
+ <message key="err.create_dir">cannot create output directory {path}: {error}</message>
126
+ <message key="err.submarine_root">expected &lt;Submarine&gt; root, found &lt;{tag}&gt;</message>
127
+ <message key="err.empty_preview_link">preview link is empty</message>
128
+ <message key="err.not_regular_file">output path is not a regular file: {path}</message>
129
+ <message key="err.backup">cannot back up {path} to {backup}: {error}</message>
130
+ <message key="err.output_exists">output file already exists: {path} (use --force)</message>
131
+ <message key="err.write">cannot write {path}: {error}</message>
132
+ <message key="err.gzip_compress">gzip compression failed: {error}</message>
133
+ <message key="err.gzip_decompress">gzip decompression failed: {error}</message>
134
+ <message key="err.decode">failed to decode as {encoding}: {error}</message>
135
+ <message key="err.decode_utf16">failed to decode as UTF-16: {error}</message>
136
+ <message key="err.invalid_decompressed_xml">decompressed data is not valid XML: {error}</message>
137
+ </locale>
@@ -0,0 +1,137 @@
1
+ <?xml version='1.0' encoding='utf-8'?>
2
+ <locale language="ru">
3
+ <message key="app.description">Преобразование файлов .sub Barotrauma в XML, удобный для Git, и обратно.</message>
4
+ <message key="metavar.command">команда</message>
5
+ <message key="arg.version">показать версию программы и выйти</message>
6
+ <message key="arg.help">показать эту справку и выйти</message>
7
+ <message key="help.usage">использование: </message>
8
+ <message key="help.positionals">позиционные аргументы</message>
9
+ <message key="help.options">параметры</message>
10
+ <message key="repl.commands">Команды</message>
11
+ <message key="repl.command.init">инициализировать/обновить рабочую папку</message>
12
+ <message key="repl.command.export">извлечь .sub и преобразовать в Git-совместимый XML</message>
13
+ <message key="repl.command.pack">упаковать XML в .sub</message>
14
+ <message key="repl.command.unpack">извлечь .sub только как исходный XML</message>
15
+ <message key="repl.command.tree">рекурсивно искать и показывать все подводные лодки в репозитории</message>
16
+ <message key="repl.command.exit">выйти из интерактивного режима</message>
17
+ <message key="repl.hint">Tab: дополнение · ↑↓ история · Ctrl-R поиск · Ctrl-D выход · ? справка</message>
18
+ <message key="repl.help"><![CDATA[<title>tree</title>
19
+ Показать файлы подводных лодок .sub в Git-репозитории в виде дерева.
20
+ - Незарегистрированные файлы помечаются красным (U)
21
+ - Зарегистрированные файлы с изменённым хешем помечаются жёлтым (M)
22
+
23
+ <title>export</title>
24
+ Экспортировать .sub в Git-совместимый XML.
25
+
26
+ <cmd>export</cmd>
27
+ Экспортировать все изменённые зарегистрированные подводные лодки
28
+ <cmd>export</cmd> "test/My Sub1.sub" "test/My Sub2.sub" ...
29
+ Экспортировать одну или несколько зарегистрированных подводных лодок
30
+ <cmd>export</cmd> "test/My Sub1.sub" -o ./mydir
31
+ Экспортировать подводную лодку в указанный каталог
32
+
33
+ Примечание: если имя подводной лодки или каталога содержит пробелы,
34
+ заключите путь в кавычки, иначе он будет разбит на несколько аргументов.
35
+
36
+ <title>pack</title>
37
+ Упаковать экспортированный XML обратно в .sub.
38
+
39
+ <cmd>pack</cmd> "test/My Sub.xml"
40
+ <cmd>pack</cmd> "test/My Sub.xml" -o ./mydir
41
+ Упаковать в указанный каталог
42
+
43
+ Примечание: если выходной .sub уже существует, он сначала сохраняется
44
+ как .sub.bak, затем перезаписывается.
45
+
46
+ <title>unpack</title>
47
+ Извлечь файл подводной лодки .sub как исходный XML. Используется редко.
48
+
49
+ <cmd>unpack</cmd> "test/My Sub.sub"
50
+ ]]></message>
51
+ <message key="repl.not_tty">интерактивный режим требует терминала; используйте подкоманду</message>
52
+ <message key="repl.parse_error">не удалось разобрать командную строку: {error}</message>
53
+ <message key="repl.workspace_missing">рабочая папка не инициализирована</message>
54
+ <message key="repl.workspace">Проект: {project} | Git repo: {git_repo} | Корень мода: {mod_root}</message>
55
+ <message key="repl.cwd">CWD: {path}</message>
56
+
57
+ <message key="command.init">инициализировать/обновить рабочую папку</message>
58
+ <message key="command.unpack">извлечь исходный XML из файлов .sub</message>
59
+ <message key="command.export">экспортировать .sub в канонический XML и изображение предпросмотра</message>
60
+ <message key="command.pack">упаковать экспортированный XML обратно в .sub</message>
61
+ <message key="command.sub">список зарегистрированных подводных лодок</message>
62
+
63
+ <message key="arg.inputs.sub">один или несколько файлов .sub</message>
64
+ <message key="arg.inputs.xml">один или несколько экспортированных XML-файлов</message>
65
+ <message key="arg.output_dir">каталог вывода; по умолчанию каталог каждого входного файла</message>
66
+ <message key="arg.encoding">принудительно задать кодировку XML вместо автоопределения UTF-8/UTF-16</message>
67
+ <message key="arg.stdout">вывести XML в stdout вместо записи в файл</message>
68
+ <message key="arg.no_validate">пропустить проверку корректности XML</message>
69
+ <message key="arg.force">перезаписывать существующие файлы без запроса</message>
70
+ <message key="arg.sort_attributes">сортировать атрибуты XML по алфавиту для стабильных diff</message>
71
+ <message key="arg.sort_attributes_pack">сортировать атрибуты XML по алфавиту перед упаковкой</message>
72
+ <message key="arg.output">выходной файл .sub или существующий каталог; по умолчанию каталог XML-файла</message>
73
+ <message key="arg.tree">показать зарегистрированные подводные лодки в виде дерева каталогов</message>
74
+ <message key="arg.force_init">принять все изменения инициализации без запроса</message>
75
+
76
+ <message key="err.stdout_single">--stdout принимает только один входной файл</message>
77
+ <message key="err.output_multi">при упаковке нескольких файлов --output должен указывать на существующий каталог</message>
78
+ <message key="prompt.overwrite">{path} уже существует. Перезаписать? [y/N] </message>
79
+ <message key="prompt.submarines">Обновить эти записи подводных лодок? [y/N] </message>
80
+ <message key="prompt.remove_submarines">Удалить эти записи подводных лодок? [y/N] </message>
81
+ <message key="prompt.mod_root_changed">Корень мода: {old} -> {new}. Обновить? [y/N] </message>
82
+ <message key="prompt.git_repo_changed">Git-репозиторий: {old} -> {new}. Обновить? [y/N] </message>
83
+ <message key="prompt.export_changed">Экспортировать эти подводные лодки? [y/N] </message>
84
+ <message key="status.skipped">пропущено: {path}</message>
85
+ <message key="status.no_changed_submarines">нет изменённых зарегистрированных подводных лодок</message>
86
+ <message key="status.export_changed_header">Зарегистрированные подводные лодки с изменениями:</message>
87
+ <message key="status.export_declined">экспорт отменён</message>
88
+ <message key="status.export_preview">{src} -> {xml}, предпросмотр: {preview}</message>
89
+ <message key="status.export_no_preview">{src} -> {xml}, без предпросмотра</message>
90
+ <message key="status.pack">{src} -> {dst}</message>
91
+ <message key="status.unpack">{src} -> {dst} [{kind}, {bytes} байт]</message>
92
+ <message key="kind.gzip">gzip</message>
93
+ <message key="kind.plain_xml">обычный XML</message>
94
+ <message key="status.git_repo">Git repo: {path}</message>
95
+
96
+ <message key="warning.current_not_repo">текущий каталог не является Git-репозиторием; используется {path}</message>
97
+ <message key="warning.no_repo">Git-репозиторий не найден до корня мода; выполните 'git init' для инициализации</message>
98
+ <message key="warning.upper_repo">у этого проекта также есть родительский Git-репозиторий: {path}</message>
99
+ <message key="warning.preview_missing">изображение предпросмотра не найдено: {path}; предпросмотр будет пустым</message>
100
+ <message key="warning.registry_preview_failed">не удалось прочитать предпросмотр для подводной лодки {name}; используется предпросмотр по умолчанию</message>
101
+ <message key="warning.decode_replace">входные данные не являются допустимым UTF-8/UTF-16; декодирование как UTF-8 с символами замены</message>
102
+
103
+ <message key="init.created_gitignore">создан {name}; добавлено {patterns}</message>
104
+ <message key="init.updated_gitignore">обновлён {name}; добавлено {patterns}</message>
105
+ <message key="init.gitignore_ok">{name} уже игнорирует {patterns}</message>
106
+ <message key="init.config_created">создан {name}</message>
107
+ <message key="init.config_updated">обновлён {name}</message>
108
+ <message key="init.config_exists">{name} уже существует</message>
109
+ <message key="init.registered_header">Зарегистрированные подводные лодки:</message>
110
+ <message key="init.pending_header">Подводные лодки для добавления или обновления:</message>
111
+ <message key="init.removed_header">Зарегистрированные подводные лодки для удаления:</message>
112
+ <message key="init.registered">зарегистрировано {count} файлов подводных лодок</message>
113
+ <message key="init.submarines_skipped">реестр подводных лодок не обновлён</message>
114
+ <message key="init.language_set">Язык установлен: русский</message>
115
+ <message key="err.init">не удалось инициализировать проект: {error}</message>
116
+ <message key="err.not_mod">это не проект Barotrauma MOD: filelist.xml не найден в {path} или его родительских каталогах</message>
117
+ <message key="err.invalid_config">недопустимая конфигурация проекта {path}: {error}</message>
118
+ <message key="err.not_registered">подводная лодка не зарегистрирована в git4subeditor.xml: {path}</message>
119
+ <message key="err.invalid_xml">недопустимый XML: {error}</message>
120
+ <message key="err.invalid_base64">previewimage не является допустимым base64: {error}</message>
121
+ <message key="err.read_file">не удалось прочитать файл {path}: {error}</message>
122
+ <message key="err.pack_sub_input">{path} похож на файл .sub; сначала экспортируйте его в XML</message>
123
+ <message key="err.read_preview">не удалось прочитать изображение предпросмотра {path}: {error}</message>
124
+ <message key="err.empty_preview">изображение предпросмотра пусто: {path}</message>
125
+ <message key="err.create_dir">не удалось создать каталог вывода {path}: {error}</message>
126
+ <message key="err.submarine_root">ожидался корневой элемент &lt;Submarine&gt;, найден &lt;{tag}&gt;</message>
127
+ <message key="err.empty_preview_link">ссылка на предпросмотр пуста</message>
128
+ <message key="err.not_regular_file">путь вывода не является обычным файлом: {path}</message>
129
+ <message key="err.backup">не удалось создать резервную копию {path} в {backup}: {error}</message>
130
+ <message key="err.output_exists">выходной файл уже существует: {path} (используйте --force)</message>
131
+ <message key="err.write">не удалось записать {path}: {error}</message>
132
+ <message key="err.gzip_compress">ошибка сжатия gzip: {error}</message>
133
+ <message key="err.gzip_decompress">ошибка распаковки gzip: {error}</message>
134
+ <message key="err.decode">не удалось декодировать как {encoding}: {error}</message>
135
+ <message key="err.decode_utf16">не удалось декодировать как UTF-16: {error}</message>
136
+ <message key="err.invalid_decompressed_xml">распакованные данные не являются допустимым XML: {error}</message>
137
+ </locale>
git4subeditor/repl.py ADDED
@@ -0,0 +1,256 @@
1
+ """Interactive command loop for git4subeditor."""
2
+
3
+ import argparse
4
+ import re
5
+ import shlex
6
+ import shutil
7
+ import sys
8
+ from functools import partial
9
+ from pathlib import Path
10
+ from typing import Any, cast
11
+
12
+ from git4subeditor import __version__
13
+ from git4subeditor.cli import _run_command, build_parser
14
+ from git4subeditor.l10n import resolve_language, set_language, t
15
+ from git4subeditor.subfile import SubFileError
16
+ from git4subeditor.utils import format_message, print_banner
17
+ from git4subeditor.workspace import (
18
+ WorkspaceInfo,
19
+ load_workspace_info,
20
+ print_submarine_paths,
21
+ )
22
+
23
+ _COMMANDS: tuple[str, ...] = ("init", "export", "pack", "unpack", "tree", "q")
24
+ _HELP_TAG = re.compile(r"<(title|cmd)>(.*?)</\1>")
25
+
26
+
27
+ def _render_help(text: str) -> str:
28
+ """Colorize help document markers."""
29
+
30
+ def replace(match: re.Match[str]) -> str:
31
+ color: str = "light_blue" if match.group(1) == "title" else "green"
32
+ return format_message(match.group(2), color=color)
33
+
34
+ return _HELP_TAG.sub(replace, text)
35
+
36
+
37
+ class _SummaryFormatter(argparse.HelpFormatter):
38
+ """Use the usage color for positional command names."""
39
+
40
+ def _format_action_invocation(self, action: argparse.Action) -> str:
41
+ theme = cast(Any, self)._theme
42
+ head, *options = action.dest.split(" [")
43
+ parts: list[str] = [f"{theme.summary_action}{head}{theme.reset}"]
44
+ for option in options:
45
+ parts.append(
46
+ f"[{theme.summary_action}{option.removesuffix(']')}{theme.reset}]"
47
+ )
48
+ return " ".join(parts)
49
+
50
+
51
+ def _print_command_list() -> None:
52
+ """Print the interactive command list in argparse style."""
53
+ commands: tuple[tuple[str, str], ...] = (
54
+ ("init", t("repl.command.init")),
55
+ ("tree", t("repl.command.tree")),
56
+ ("export <file.sub> [-o DIR]", t("repl.command.export")),
57
+ ("pack <file.xml> [-o FILE]", t("repl.command.pack")),
58
+ ("unpack <file.sub> [-o DIR]", t("repl.command.unpack")),
59
+ ("q", t("repl.command.exit")),
60
+ )
61
+ columns: int = shutil.get_terminal_size(fallback=(80, 24)).columns
62
+ max_command: int = max(len(syntax) for syntax, _ in commands)
63
+ max_description: int = max(len(description) for _, description in commands)
64
+ show_descriptions: bool = columns >= max(
65
+ max_command + max_description + 6,
66
+ max_command + 26,
67
+ )
68
+ parser = argparse.ArgumentParser(
69
+ prog="",
70
+ add_help=False,
71
+ formatter_class=partial(_SummaryFormatter, max_help_position=40),
72
+ )
73
+ formatter = parser._get_formatter()
74
+ formatter.start_section(t("repl.commands"))
75
+ for syntax, description in commands:
76
+ formatter.add_argument(
77
+ argparse.Action(
78
+ [],
79
+ syntax,
80
+ help=description if show_descriptions else None,
81
+ )
82
+ )
83
+ formatter.end_section()
84
+ print(formatter.format_help(), end="")
85
+
86
+
87
+ def _print_workspace_info(info: WorkspaceInfo | None) -> None:
88
+ """Print the interactive workspace summary.
89
+
90
+ Args:
91
+ info: Loaded workspace information, or None when the workspace is not
92
+ initialized.
93
+ """
94
+ if info is None:
95
+ print(t("repl.workspace_missing"))
96
+ return
97
+
98
+ print(
99
+ t(
100
+ "repl.workspace",
101
+ project=info.project_name,
102
+ git_repo=info.git_repo,
103
+ mod_root=info.mod_root,
104
+ )
105
+ )
106
+ print(t("repl.cwd", path=Path.cwd().resolve()))
107
+ print_submarine_paths(info)
108
+
109
+
110
+ def _workspace_prompt(info: WorkspaceInfo | None) -> str:
111
+ """Return the interactive prompt for the current workspace."""
112
+ prefix: str = format_message("g4s", color="light_blue")
113
+ if info is None:
114
+ return f"{prefix}> "
115
+ return f"{prefix}:{format_message(info.project_name, color='green')}> "
116
+
117
+
118
+ def _split_interactive_line(line: str) -> list[str]:
119
+ """Split one interactive command line without mangling Windows paths.
120
+
121
+ Args:
122
+ line: Raw text entered at the interactive prompt.
123
+
124
+ Returns:
125
+ Command tokens with matching outer quotes removed.
126
+
127
+ Raises:
128
+ ValueError: If the line contains an unmatched quote.
129
+ """
130
+ tokens: list[str] = shlex.split(line, posix=False)
131
+ return [
132
+ token[1:-1]
133
+ if len(token) >= 2 and token[0] == token[-1] and token[0] in "\"'"
134
+ else token
135
+ for token in tokens
136
+ ]
137
+
138
+
139
+ def run() -> int:
140
+ """Run the interactive command loop.
141
+
142
+ Returns:
143
+ Process exit code.
144
+ """
145
+ if sys.stdin is None or not sys.stdin.isatty():
146
+ print(
147
+ format_message(t("repl.not_tty"), label="error"),
148
+ file=sys.stderr,
149
+ )
150
+ return 2
151
+
152
+ print_banner(__version__)
153
+ print()
154
+ try:
155
+ workspace: WorkspaceInfo | None = load_workspace_info()
156
+ except SubFileError as exc:
157
+ print(format_message(str(exc), label="error"), file=sys.stderr)
158
+ return 1
159
+ _print_workspace_info(workspace)
160
+ print()
161
+ _print_command_list()
162
+ print()
163
+ print(t("repl.hint"))
164
+ prompt: str = _workspace_prompt(workspace)
165
+ session: Any = None
166
+ ansi: Any = None
167
+ try:
168
+ from prompt_toolkit import PromptSession
169
+ from prompt_toolkit.completion import Completer, Completion
170
+ from prompt_toolkit.formatted_text import ANSI
171
+ from prompt_toolkit.history import FileHistory
172
+ except ImportError:
173
+ pass
174
+ else:
175
+
176
+ class _CommandCompleter(Completer):
177
+ """Complete REPL command names."""
178
+
179
+ def get_completions(
180
+ self,
181
+ document: Any,
182
+ complete_event: Any,
183
+ ) -> Any:
184
+ """Yield completions for the first token."""
185
+ text: str = document.text_before_cursor
186
+ if " " in text:
187
+ return
188
+ for command in _COMMANDS:
189
+ if command.startswith(text):
190
+ yield Completion(command, start_position=-len(text))
191
+
192
+ session = PromptSession(
193
+ history=FileHistory(str(Path.cwd() / "history")),
194
+ completer=_CommandCompleter(),
195
+ complete_while_typing=False,
196
+ )
197
+ ansi = ANSI
198
+
199
+ while True:
200
+ try:
201
+ if session is None:
202
+ line: str = input(prompt)
203
+ else:
204
+ line = session.prompt(ansi(prompt))
205
+ except EOFError:
206
+ print()
207
+ return 0
208
+ except KeyboardInterrupt:
209
+ print()
210
+ continue
211
+
212
+ try:
213
+ tokens: list[str] = _split_interactive_line(line)
214
+ except ValueError as exc:
215
+ print(
216
+ format_message(t("repl.parse_error", error=exc), label="error"),
217
+ file=sys.stderr,
218
+ )
219
+ continue
220
+ if not tokens:
221
+ continue
222
+
223
+ name: str = tokens[0].casefold()
224
+ if name == "q":
225
+ return 0
226
+ if name == "?":
227
+ print(_render_help(t("repl.help")), end="")
228
+ continue
229
+ if name == "tree":
230
+ if workspace is None:
231
+ print(t("repl.workspace_missing"))
232
+ else:
233
+ print_submarine_paths(workspace)
234
+ continue
235
+
236
+ try:
237
+ args: argparse.Namespace = build_parser().parse_args(tokens)
238
+ except SystemExit:
239
+ continue
240
+ except KeyboardInterrupt:
241
+ print()
242
+ continue
243
+
244
+ try:
245
+ code: int = _run_command(args, show_banner=False)
246
+ except KeyboardInterrupt:
247
+ print()
248
+ continue
249
+ if args.command == "init" and code == 0:
250
+ set_language(resolve_language())
251
+ try:
252
+ workspace = load_workspace_info()
253
+ except SubFileError:
254
+ pass
255
+ else:
256
+ prompt = _workspace_prompt(workspace)