yd-cli 0.3__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.
- yd/__init__.py +30 -0
- yd/__main__.py +11 -0
- yd/cli.py +484 -0
- yd/commands.py +255 -0
- yd/execution.py +153 -0
- yd/io.py +200 -0
- yd/py.typed +0 -0
- yd/types.py +159 -0
- yd_cli-0.3.dist-info/METADATA +132 -0
- yd_cli-0.3.dist-info/RECORD +13 -0
- yd_cli-0.3.dist-info/WHEEL +4 -0
- yd_cli-0.3.dist-info/entry_points.txt +3 -0
- yd_cli-0.3.dist-info/licenses/LICENSES/MIT.txt +9 -0
yd/__init__.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Christian Heinze
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
"""The yd synchronization tool.
|
|
5
|
+
|
|
6
|
+
Do not directly import items from this module (e.g. `from yd import io`) which
|
|
7
|
+
triggers `__getattr__` and thereby eager importing of the module. Instead, import
|
|
8
|
+
`yd` and use attribute access.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import importlib
|
|
14
|
+
import importlib.metadata
|
|
15
|
+
|
|
16
|
+
TYPE_CHECKING = False
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from types import ModuleType
|
|
19
|
+
|
|
20
|
+
from yd import commands, execution, io, types
|
|
21
|
+
else:
|
|
22
|
+
|
|
23
|
+
def __getattr__(key: str, /) -> ModuleType:
|
|
24
|
+
if key in __all__:
|
|
25
|
+
return importlib.import_module(f"{__package__}.{key}", package=__package__)
|
|
26
|
+
raise AttributeError(f"module `{__name__}` has not attribute `{key}`")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
__version__ = importlib.metadata.version("yd-cli")
|
|
30
|
+
__all__ = ["__version__", "commands", "io", "execution", "types"]
|
yd/__main__.py
ADDED
yd/cli.py
ADDED
|
@@ -0,0 +1,484 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: Christian Heinze
|
|
2
|
+
#
|
|
3
|
+
# SPDX-License-Identifier: MIT
|
|
4
|
+
"""CLI."""
|
|
5
|
+
|
|
6
|
+
# ruff: noqa: PLC0415
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import dataclasses
|
|
11
|
+
import itertools as it
|
|
12
|
+
import re
|
|
13
|
+
import sys
|
|
14
|
+
from typing import TYPE_CHECKING, Annotated
|
|
15
|
+
|
|
16
|
+
import rich.console
|
|
17
|
+
import rich.highlighter
|
|
18
|
+
import rich.theme
|
|
19
|
+
import typer
|
|
20
|
+
|
|
21
|
+
import yd
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
import enum
|
|
25
|
+
from collections.abc import Callable
|
|
26
|
+
from collections.abc import Set as AbstractSet
|
|
27
|
+
|
|
28
|
+
from rich.text import Text
|
|
29
|
+
|
|
30
|
+
env: yd.types.Environment
|
|
31
|
+
console: rich.console.Console
|
|
32
|
+
highlighter: StrEnumHighlighter
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclasses.dataclass(slots=True)
|
|
36
|
+
class StrEnumHighlighter[T: enum.StrEnum](
|
|
37
|
+
# Solely defines a __call__ method wrapping the abstract method `highlight`.
|
|
38
|
+
rich.highlighter.Highlighter
|
|
39
|
+
):
|
|
40
|
+
enum_type: type[T]
|
|
41
|
+
|
|
42
|
+
_: dataclasses.KW_ONLY
|
|
43
|
+
ignore: AbstractSet[T] = dataclasses.field(default_factory=set)
|
|
44
|
+
base_style_name: str | None = None
|
|
45
|
+
|
|
46
|
+
@property
|
|
47
|
+
def base_style(self) -> str:
|
|
48
|
+
return (
|
|
49
|
+
self.enum_type.__qualname__.lower()
|
|
50
|
+
if self.base_style_name is None
|
|
51
|
+
else self.base_style_name
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def highlight(self, text: Text) -> None:
|
|
55
|
+
base_style, hl = self.base_style, text.highlight_regex
|
|
56
|
+
for cat in self.enum_type:
|
|
57
|
+
if cat in self.ignore:
|
|
58
|
+
continue
|
|
59
|
+
# Using `__str__` instead of `cat.name.lower()` for more flexibility.
|
|
60
|
+
hl(rf"(?P<{cat}>{cat})", style_prefix=base_style)
|
|
61
|
+
|
|
62
|
+
def create_theme_addon(self, *, styler: Callable[[T], str]) -> dict[str, str]:
|
|
63
|
+
base_style = self.base_style
|
|
64
|
+
return {
|
|
65
|
+
base_style + str(cat): styler(cat)
|
|
66
|
+
for cat in self.enum_type
|
|
67
|
+
if cat not in self.ignore
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _action_styler(action: yd.types.Action) -> str:
|
|
72
|
+
match action:
|
|
73
|
+
case yd.types.Action.CREATE:
|
|
74
|
+
return "bold #000020 on #0050aa"
|
|
75
|
+
case yd.types.Action.COPY:
|
|
76
|
+
return "bold white on #006020"
|
|
77
|
+
case yd.types.Action.DELETE:
|
|
78
|
+
return "bold #200000 on #aa0000"
|
|
79
|
+
case yd.types.Action.HARDLINK:
|
|
80
|
+
return "bold white on #606060"
|
|
81
|
+
case yd.types.Action.ATTRUPDATE:
|
|
82
|
+
return "bold white on #606060"
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _setup_rich_devices() -> tuple[rich.console.Console, StrEnumHighlighter]:
|
|
86
|
+
highlighter = StrEnumHighlighter(
|
|
87
|
+
yd.types.Action, ignore={yd.types.Action.ATTRUPDATE}
|
|
88
|
+
)
|
|
89
|
+
theme = rich.theme.Theme(
|
|
90
|
+
highlighter.create_theme_addon(styler=_action_styler)
|
|
91
|
+
| {
|
|
92
|
+
"progress.elapsed": "bold #ffffaa",
|
|
93
|
+
"info": "italic #000090",
|
|
94
|
+
"warning": "bold #900000",
|
|
95
|
+
"error": "bold #ff0000",
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
console = rich.console.Console(highlighter=highlighter, theme=theme)
|
|
99
|
+
return console, highlighter
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
_YD_HELP = """
|
|
103
|
+
Build and execute `rsync` commands from TOML configuration files.
|
|
104
|
+
|
|
105
|
+
Configuration files live in `~/.config/yd/`. If `XDG_CONFIG_HOME` is set, it
|
|
106
|
+
replaces only the `~/.config` part, so configurations are loaded from
|
|
107
|
+
`$XDG_CONFIG_HOME/yd/`. Relative `src_home` and `target_home` paths are
|
|
108
|
+
resolved from your home directory. Use `yd edit NAME` to edit an existing
|
|
109
|
+
configuration, or `yd edit --new NAME` to create one; `EDITOR` must be set for
|
|
110
|
+
this command. Use `yd run NAME` to execute a configuration and `yd ls` to show
|
|
111
|
+
available configurations together with their descriptions.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
app = typer.Typer(help=_YD_HELP, no_args_is_help=True, rich_markup_mode="markdown")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@app.callback()
|
|
118
|
+
def _setup_io() -> None:
|
|
119
|
+
global env, console, highlighter # noqa: PLW0603
|
|
120
|
+
|
|
121
|
+
import logging
|
|
122
|
+
from pathlib import Path
|
|
123
|
+
|
|
124
|
+
console, highlighter = _setup_rich_devices()
|
|
125
|
+
try:
|
|
126
|
+
env = yd.io.capture_environment()
|
|
127
|
+
except yd.types.EnvCaptureError as err:
|
|
128
|
+
console.print("Failed to capture environment.", style="error")
|
|
129
|
+
raise typer.Exit(2) from err
|
|
130
|
+
|
|
131
|
+
if env.log_level is not None:
|
|
132
|
+
logging.basicConfig(
|
|
133
|
+
filename=Path.cwd() / ".yd.log",
|
|
134
|
+
encoding="utf-8",
|
|
135
|
+
level=env.log_level,
|
|
136
|
+
force=True,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _description_column_width(min_width: int = 50) -> int:
|
|
141
|
+
import shutil
|
|
142
|
+
|
|
143
|
+
non_desc_with = (
|
|
144
|
+
10 # time elapsed
|
|
145
|
+
+ 2 # dir symbol
|
|
146
|
+
+ sum(
|
|
147
|
+
len(cat.name) + 10
|
|
148
|
+
for cat in highlighter.enum_type
|
|
149
|
+
if cat not in highlighter.ignore
|
|
150
|
+
)
|
|
151
|
+
)
|
|
152
|
+
return max(min_width, shutil.get_terminal_size().columns - non_desc_with)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _create_description(path: str, *, max_len: int, prefix: str = "...") -> str:
|
|
156
|
+
from pathlib import Path
|
|
157
|
+
|
|
158
|
+
if len(label := path) <= max_len:
|
|
159
|
+
return label
|
|
160
|
+
|
|
161
|
+
path_ = Path(path)
|
|
162
|
+
reversed_chosen_parts = tuple(
|
|
163
|
+
part
|
|
164
|
+
for part, cum_len in zip(
|
|
165
|
+
reversed(path_.parts),
|
|
166
|
+
it.accumulate(len(s) for s in reversed(path_.parts)),
|
|
167
|
+
strict=True,
|
|
168
|
+
)
|
|
169
|
+
if cum_len <= max_len
|
|
170
|
+
)
|
|
171
|
+
base = "/".join(reversed(reversed_chosen_parts))
|
|
172
|
+
if len(base) <= max_len:
|
|
173
|
+
return base
|
|
174
|
+
return f"{prefix}{base[-(max_len - len(prefix)) :]}"
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# Use `list` as return value since type must be available at runtime for typer to work.
|
|
178
|
+
def _complete_config_name(incomplete: str, /) -> list[str]:
|
|
179
|
+
return [
|
|
180
|
+
path.stem
|
|
181
|
+
for path in env.config_dir.glob("*.toml")
|
|
182
|
+
if re.match(incomplete, path.stem, re.IGNORECASE)
|
|
183
|
+
]
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _read_from_stdin(opt: str, /) -> str:
|
|
187
|
+
value = sys.stdin.read().strip()
|
|
188
|
+
if not value:
|
|
189
|
+
raise typer.BadParameter(f"no input via STDIN; `{opt}` cannot be determined")
|
|
190
|
+
return value
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _resolve_home(
|
|
194
|
+
*, src: str | None, target: str | None
|
|
195
|
+
) -> tuple[str | None, str | None]:
|
|
196
|
+
if src == "-" and target == "-":
|
|
197
|
+
raise typer.BadParameter(
|
|
198
|
+
"only one of `src-home` and `target-home` can be read from stdin."
|
|
199
|
+
)
|
|
200
|
+
if src == "-":
|
|
201
|
+
return _read_from_stdin("src-home"), target
|
|
202
|
+
if target == "-":
|
|
203
|
+
return src, _read_from_stdin("target-home")
|
|
204
|
+
return src, target
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _require_nonempty(param: typer.CallbackParam, v: str | None) -> str | None:
|
|
208
|
+
if v is None or v:
|
|
209
|
+
return v
|
|
210
|
+
raise typer.BadParameter(f"parameter `{param.name}` must not be an empty string")
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@app.command()
|
|
214
|
+
def run(
|
|
215
|
+
# Considered allowing absolute path here (to skip lookup logic). However, can be
|
|
216
|
+
# handled via setting an environment variable; not extra handling needed.
|
|
217
|
+
cfg_name: Annotated[
|
|
218
|
+
str,
|
|
219
|
+
typer.Argument(
|
|
220
|
+
help="Configuration name. Must match the TOML file name without `.toml`.",
|
|
221
|
+
autocompletion=_complete_config_name,
|
|
222
|
+
callback=_require_nonempty,
|
|
223
|
+
),
|
|
224
|
+
],
|
|
225
|
+
*,
|
|
226
|
+
no_backup: Annotated[
|
|
227
|
+
bool,
|
|
228
|
+
typer.Option(
|
|
229
|
+
"--no-backup",
|
|
230
|
+
help="Disable backup handling for this run.",
|
|
231
|
+
rich_help_panel="Sync options",
|
|
232
|
+
),
|
|
233
|
+
] = False,
|
|
234
|
+
keep_newer: Annotated[
|
|
235
|
+
bool,
|
|
236
|
+
typer.Option(
|
|
237
|
+
"--keep-newer",
|
|
238
|
+
help="Skip updates when the target file is newer.",
|
|
239
|
+
rich_help_panel="Sync options",
|
|
240
|
+
),
|
|
241
|
+
] = False,
|
|
242
|
+
rename_speedup: Annotated[
|
|
243
|
+
bool,
|
|
244
|
+
typer.Option(
|
|
245
|
+
"--rename-speedup",
|
|
246
|
+
help="Enable rsync options tuned for rename-heavy targets."
|
|
247
|
+
" This may require more disk space on the target.",
|
|
248
|
+
rich_help_panel="Sync options",
|
|
249
|
+
),
|
|
250
|
+
] = False,
|
|
251
|
+
dry_run: Annotated[
|
|
252
|
+
bool,
|
|
253
|
+
typer.Option(
|
|
254
|
+
"--dry-run",
|
|
255
|
+
help="Show what would happen without changing files.",
|
|
256
|
+
),
|
|
257
|
+
] = False,
|
|
258
|
+
src_home: Annotated[
|
|
259
|
+
str | None,
|
|
260
|
+
typer.Option(
|
|
261
|
+
"--src-home",
|
|
262
|
+
rich_help_panel="Location options",
|
|
263
|
+
help="Override `src_home` from the configuration."
|
|
264
|
+
" Use '-' to read the value from stdin.",
|
|
265
|
+
callback=_require_nonempty,
|
|
266
|
+
),
|
|
267
|
+
] = None,
|
|
268
|
+
target_home: Annotated[
|
|
269
|
+
str | None,
|
|
270
|
+
typer.Option(
|
|
271
|
+
"--target-home",
|
|
272
|
+
rich_help_panel="Location options",
|
|
273
|
+
help="Override `target_home` from the configuration."
|
|
274
|
+
" Use '-' to read the value from stdin.",
|
|
275
|
+
callback=_require_nonempty,
|
|
276
|
+
),
|
|
277
|
+
] = None,
|
|
278
|
+
) -> int:
|
|
279
|
+
"""Run a saved configuration by name."""
|
|
280
|
+
import asyncio
|
|
281
|
+
import functools
|
|
282
|
+
from pathlib import Path
|
|
283
|
+
|
|
284
|
+
import rich.progress
|
|
285
|
+
import rich.table
|
|
286
|
+
|
|
287
|
+
try:
|
|
288
|
+
cfg = yd.io.load_command_group(env.config_dir / f"{cfg_name}.toml")
|
|
289
|
+
except yd.types.ConfigLoadError as err:
|
|
290
|
+
console.print(str(err), style="error")
|
|
291
|
+
raise typer.Exit(2) from err
|
|
292
|
+
|
|
293
|
+
res_src_home, res_target_home = _resolve_home(src=src_home, target=target_home)
|
|
294
|
+
try:
|
|
295
|
+
commands = list(
|
|
296
|
+
yd.commands.create(
|
|
297
|
+
cfg,
|
|
298
|
+
env=env,
|
|
299
|
+
src_home=res_src_home,
|
|
300
|
+
target_home=res_target_home,
|
|
301
|
+
dry_run=dry_run,
|
|
302
|
+
keep_newer=keep_newer,
|
|
303
|
+
rename_speedup=rename_speedup,
|
|
304
|
+
no_backup=no_backup,
|
|
305
|
+
error_encoder=yd.io.encode_error,
|
|
306
|
+
)
|
|
307
|
+
)
|
|
308
|
+
except yd.types.CommandBuildError as err:
|
|
309
|
+
console.print(str(err), style="error")
|
|
310
|
+
raise typer.Exit(2) from err
|
|
311
|
+
|
|
312
|
+
with (
|
|
313
|
+
asyncio.Runner() as runner,
|
|
314
|
+
rich.progress.Progress(
|
|
315
|
+
rich.progress.TimeElapsedColumn(table_column=rich.table.Column(width=8)),
|
|
316
|
+
rich.progress.TextColumn(
|
|
317
|
+
":file_folder:",
|
|
318
|
+
table_column=rich.table.Column(width=2),
|
|
319
|
+
),
|
|
320
|
+
rich.progress.TextColumn(
|
|
321
|
+
"{task.description}",
|
|
322
|
+
table_column=rich.table.Column(width=_description_column_width()),
|
|
323
|
+
),
|
|
324
|
+
*it.chain.from_iterable(
|
|
325
|
+
(
|
|
326
|
+
rich.progress.TextColumn(
|
|
327
|
+
action_name,
|
|
328
|
+
justify="right",
|
|
329
|
+
highlighter=highlighter,
|
|
330
|
+
table_column=rich.table.Column(width=len(action_name)),
|
|
331
|
+
),
|
|
332
|
+
rich.progress.TextColumn(
|
|
333
|
+
f"{{task.fields[{action_name}]}}",
|
|
334
|
+
justify="right",
|
|
335
|
+
table_column=rich.table.Column(min_width=7),
|
|
336
|
+
),
|
|
337
|
+
)
|
|
338
|
+
for action_name in (
|
|
339
|
+
str(cat)
|
|
340
|
+
for cat in highlighter.enum_type
|
|
341
|
+
if cat not in highlighter.ignore
|
|
342
|
+
)
|
|
343
|
+
),
|
|
344
|
+
console=console,
|
|
345
|
+
) as progress,
|
|
346
|
+
):
|
|
347
|
+
template = "{action} {filename} ({transfer_bytes} bytes)"
|
|
348
|
+
|
|
349
|
+
async def show_progress(
|
|
350
|
+
item: str | yd.types.Transaction | None,
|
|
351
|
+
*,
|
|
352
|
+
counter: dict[str, int],
|
|
353
|
+
taskid: rich.progress.TaskID,
|
|
354
|
+
target: Path,
|
|
355
|
+
) -> None:
|
|
356
|
+
match item:
|
|
357
|
+
case str() as text:
|
|
358
|
+
console.print(text, style="info")
|
|
359
|
+
return
|
|
360
|
+
case yd.types.Transaction() as transaction:
|
|
361
|
+
if transaction.action is yd.types.Action.ATTRUPDATE:
|
|
362
|
+
return
|
|
363
|
+
|
|
364
|
+
console.print(
|
|
365
|
+
template.format(
|
|
366
|
+
action=transaction.action,
|
|
367
|
+
filename=target.joinpath(transaction.filename).as_posix()
|
|
368
|
+
if not Path(transaction.filename).is_absolute()
|
|
369
|
+
else transaction.filename,
|
|
370
|
+
transfer_bytes=transaction.transfer_bytes,
|
|
371
|
+
)
|
|
372
|
+
)
|
|
373
|
+
counter[str(transaction.action)] += 1
|
|
374
|
+
progress.update(
|
|
375
|
+
taskid,
|
|
376
|
+
**counter, # type: ignore[ty:invalid-argument-type]
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
for binary, args in commands:
|
|
380
|
+
counter = {str(action): 0 for action in yd.types.Action}
|
|
381
|
+
taskid = progress.add_task(
|
|
382
|
+
description=_create_description(
|
|
383
|
+
args[-1], max_len=_description_column_width()
|
|
384
|
+
),
|
|
385
|
+
**counter, # type: ignore[ty:invalid-argument-type]
|
|
386
|
+
total=1,
|
|
387
|
+
)
|
|
388
|
+
try:
|
|
389
|
+
runner.run(
|
|
390
|
+
yd.execution.run(
|
|
391
|
+
binary,
|
|
392
|
+
*args,
|
|
393
|
+
parser_stdout=yd.io.decode_from_stdout,
|
|
394
|
+
parser_stderr=yd.io.decode_from_stderr,
|
|
395
|
+
consumer=functools.partial(
|
|
396
|
+
show_progress,
|
|
397
|
+
counter=counter,
|
|
398
|
+
taskid=taskid,
|
|
399
|
+
target=Path(args[-1]),
|
|
400
|
+
),
|
|
401
|
+
)
|
|
402
|
+
)
|
|
403
|
+
except (yd.types.OutputParseError, yd.types.OutputConsumeError) as err:
|
|
404
|
+
console.print("Failed to handle rsync output.", style="error")
|
|
405
|
+
raise typer.Exit(2) from err
|
|
406
|
+
progress.update(taskid, completed=1)
|
|
407
|
+
|
|
408
|
+
return 0
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
@app.command(name="ls")
|
|
412
|
+
def list_configs() -> int:
|
|
413
|
+
"""List available configurations and their descriptions."""
|
|
414
|
+
import rich.table
|
|
415
|
+
|
|
416
|
+
configs = list(yd.io.list_available_configs(env.config_dir))
|
|
417
|
+
if not configs:
|
|
418
|
+
console.print("No configurations found.", style="warning")
|
|
419
|
+
return 0
|
|
420
|
+
|
|
421
|
+
table = rich.table.Table()
|
|
422
|
+
table.add_column("Configuration", style="bold")
|
|
423
|
+
table.add_column("Description", style="bold")
|
|
424
|
+
for config in configs:
|
|
425
|
+
table.add_row(config.name, config.description or "")
|
|
426
|
+
console.print(table)
|
|
427
|
+
return 0
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
@app.command()
|
|
431
|
+
def edit(
|
|
432
|
+
cfg_name: Annotated[
|
|
433
|
+
str | None,
|
|
434
|
+
typer.Argument(
|
|
435
|
+
help="Configuration name. Must match the TOML file name without `.toml`.",
|
|
436
|
+
autocompletion=_complete_config_name,
|
|
437
|
+
callback=_require_nonempty,
|
|
438
|
+
),
|
|
439
|
+
] = None,
|
|
440
|
+
*,
|
|
441
|
+
new: Annotated[
|
|
442
|
+
bool,
|
|
443
|
+
typer.Option("--new", help="Create a new configuration before opening it."),
|
|
444
|
+
] = False,
|
|
445
|
+
) -> int:
|
|
446
|
+
"""Open an existing configuration or create a new one in the editor."""
|
|
447
|
+
import contextlib
|
|
448
|
+
import subprocess
|
|
449
|
+
|
|
450
|
+
if (editor_bin := env.editor_bin) is None:
|
|
451
|
+
raise typer.BadParameter("environment variable `EDITOR` is not set")
|
|
452
|
+
|
|
453
|
+
is_nvim = False
|
|
454
|
+
with contextlib.suppress(subprocess.CalledProcessError):
|
|
455
|
+
vers = subprocess.run(
|
|
456
|
+
[editor_bin, "--version"], encoding="utf-8", capture_output=True, check=True
|
|
457
|
+
).stdout
|
|
458
|
+
is_nvim = re.match(r"\s*NVIM", vers) is not None
|
|
459
|
+
|
|
460
|
+
config_path = env.config_dir / f"{cfg_name}.toml"
|
|
461
|
+
if config_path.exists():
|
|
462
|
+
if new:
|
|
463
|
+
raise typer.BadParameter(f"Configuration '{cfg_name}' already exists.")
|
|
464
|
+
args: tuple[str, ...] = (config_path.as_posix(),)
|
|
465
|
+
input_: str | None = None
|
|
466
|
+
elif new and is_nvim:
|
|
467
|
+
args = (
|
|
468
|
+
"-",
|
|
469
|
+
"+setlocal filetype=toml",
|
|
470
|
+
f"+file {config_path.as_posix()}",
|
|
471
|
+
)
|
|
472
|
+
input_ = yd.commands.CONFIG_TEMPLATE.lstrip()
|
|
473
|
+
elif new:
|
|
474
|
+
config_path.write_text(yd.commands.CONFIG_TEMPLATE.lstrip(), encoding="utf-8")
|
|
475
|
+
args, input_ = (config_path.as_posix(),), None
|
|
476
|
+
else:
|
|
477
|
+
raise typer.BadParameter(
|
|
478
|
+
f"Configuration '{cfg_name}' does not exist. Use '--new' to create it."
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
completed = subprocess.run(
|
|
482
|
+
(editor_bin, *args), input=input_, encoding="utf-8", check=False
|
|
483
|
+
)
|
|
484
|
+
return completed.returncode
|