treeing 1.0.1__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.
- treeing/__init__.py +7 -0
- treeing/_release_defaults.py +3 -0
- treeing/assets/icon.icns +0 -0
- treeing/assets/icon.ico +0 -0
- treeing/assets/icon.png +0 -0
- treeing/cli/__init__.py +5 -0
- treeing/cli/confirm.py +197 -0
- treeing/cli/help_text.py +311 -0
- treeing/cli/io.py +72 -0
- treeing/cli/main.py +427 -0
- treeing/cli/report.py +104 -0
- treeing/cli_entry.py +16 -0
- treeing/config.py +205 -0
- treeing/core/__init__.py +15 -0
- treeing/core/constants.py +20 -0
- treeing/core/generator.py +567 -0
- treeing/core/parser.py +407 -0
- treeing/core/preview.py +98 -0
- treeing/gui/__init__.py +5 -0
- treeing/gui/app.py +886 -0
- treeing/gui/dnd.py +124 -0
- treeing/gui/icon.py +69 -0
- treeing/gui/preview.py +9 -0
- treeing/gui/settings.py +80 -0
- treeing/gui/tooltip.py +234 -0
- treeing/gui_entry.py +39 -0
- treeing/main.py +21 -0
- treeing/path_checks.py +87 -0
- treeing/strings.bootstrap.json +7 -0
- treeing/strings.json +222 -0
- treeing-1.0.1.dist-info/METADATA +112 -0
- treeing-1.0.1.dist-info/RECORD +36 -0
- treeing-1.0.1.dist-info/WHEEL +5 -0
- treeing-1.0.1.dist-info/entry_points.txt +3 -0
- treeing-1.0.1.dist-info/licenses/LICENSE +21 -0
- treeing-1.0.1.dist-info/top_level.txt +1 -0
treeing/__init__.py
ADDED
treeing/assets/icon.icns
ADDED
|
Binary file
|
treeing/assets/icon.ico
ADDED
|
Binary file
|
treeing/assets/icon.png
ADDED
|
Binary file
|
treeing/cli/__init__.py
ADDED
treeing/cli/confirm.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""
|
|
2
|
+
treeing/cli/confirm.py
|
|
3
|
+
|
|
4
|
+
Defines the pre-write confirmation logic: interactive --confirm, headless
|
|
5
|
+
--yes, and the TREEING_YES environment variable. Provides
|
|
6
|
+
`gate_before_write` as the unified entry point, handling TTY detection,
|
|
7
|
+
confirmation prompts and mode resolution.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
from ..config import get_string
|
|
16
|
+
from .io import cli_err, cli_out, cli_warn
|
|
17
|
+
|
|
18
|
+
ENV_ASSUME_YES = 'TREEING_YES'
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def assume_yes_from_env() -> bool:
|
|
22
|
+
"""
|
|
23
|
+
Check whether the TREEING_YES environment variable is truthy (1/true/yes/on).
|
|
24
|
+
|
|
25
|
+
Used by gate_before_write to auto-confirm in non-interactive or CI
|
|
26
|
+
environments; the check is case-insensitive.
|
|
27
|
+
"""
|
|
28
|
+
val = os.environ.get(ENV_ASSUME_YES, '').strip().lower()
|
|
29
|
+
if not val:
|
|
30
|
+
return False
|
|
31
|
+
return val in ('1', 'true', 'yes', 'on')
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def is_interactive_tty() -> bool:
|
|
35
|
+
"""
|
|
36
|
+
Return whether the current session is an interactive TTY (both stdin and stderr are ttys).
|
|
37
|
+
|
|
38
|
+
Only treats the session as interactive when both are ttys; returns False
|
|
39
|
+
under pipes or redirection.
|
|
40
|
+
"""
|
|
41
|
+
return bool(sys.stdin.isatty() and sys.stderr.isatty())
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def read_yes_no(*, default: bool = False) -> bool:
|
|
45
|
+
"""
|
|
46
|
+
Read a y/n answer from the user; the default is controlled by `default`.
|
|
47
|
+
|
|
48
|
+
Windows uses msvcrt; Unix uses /dev/tty.
|
|
49
|
+
"""
|
|
50
|
+
suffix = '[Y/n] ' if default else '[y/N] '
|
|
51
|
+
cli_err(get_string('cli_confirm_prompt', suffix=suffix), end='')
|
|
52
|
+
if sys.platform == 'win32':
|
|
53
|
+
return _read_yes_no_windows(default=default)
|
|
54
|
+
return _read_yes_no_unix(default=default)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _read_yes_no_windows(*, default: bool) -> bool:
|
|
58
|
+
"""
|
|
59
|
+
Read a single y/n character on Windows (no Enter needed).
|
|
60
|
+
|
|
61
|
+
Accepts only y/n; Enter falls back to the default; other keys keep waiting.
|
|
62
|
+
"""
|
|
63
|
+
import msvcrt
|
|
64
|
+
|
|
65
|
+
while True:
|
|
66
|
+
ch = msvcrt.getwch()
|
|
67
|
+
if ch in '\r\n':
|
|
68
|
+
cli_err('')
|
|
69
|
+
return default
|
|
70
|
+
lower = ch.lower()
|
|
71
|
+
if lower in ('y', 'n'):
|
|
72
|
+
cli_err(ch)
|
|
73
|
+
return lower == 'y'
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _read_yes_no_unix(*, default: bool) -> bool:
|
|
77
|
+
"""
|
|
78
|
+
Read a line on Unix (via /dev/tty to avoid pipe interference).
|
|
79
|
+
|
|
80
|
+
Returns the default on a blank line or read failure; only the first
|
|
81
|
+
character is consulted.
|
|
82
|
+
"""
|
|
83
|
+
try:
|
|
84
|
+
with open('/dev/tty', encoding='utf-8', errors='replace') as tty:
|
|
85
|
+
line = tty.readline()
|
|
86
|
+
except OSError:
|
|
87
|
+
return default
|
|
88
|
+
answer = line.strip().lower()
|
|
89
|
+
if not answer:
|
|
90
|
+
return default
|
|
91
|
+
return answer in ('y', 'yes')
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def warn_confirm_ignored_on_dry_run() -> None:
|
|
95
|
+
"""
|
|
96
|
+
Warn that --confirm is ignored under --dry-run.
|
|
97
|
+
|
|
98
|
+
Because dry-run writes nothing, asking the user to confirm is pointless.
|
|
99
|
+
"""
|
|
100
|
+
cli_warn(get_string('cli_confirm_dry_run_ignored'))
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def audit_yes_confirmed(*, path: str, node_count: int, quiet: bool) -> None:
|
|
104
|
+
"""
|
|
105
|
+
In headless mode (--yes / env), print a confirmation summary.
|
|
106
|
+
|
|
107
|
+
Even though the interactive prompt is skipped, the log still shows
|
|
108
|
+
"confirmed: N nodes will be created".
|
|
109
|
+
"""
|
|
110
|
+
if quiet:
|
|
111
|
+
return
|
|
112
|
+
cli_out(get_string('cli_confirm_yes_audit', count=node_count, path=path))
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def prompt_generate_confirm(
|
|
116
|
+
*,
|
|
117
|
+
path: str,
|
|
118
|
+
node_count: int,
|
|
119
|
+
fail_on_conflict: bool,
|
|
120
|
+
warn_count: int,
|
|
121
|
+
) -> bool:
|
|
122
|
+
"""
|
|
123
|
+
In interactive mode, print the generation confirmation prompt and read the user's answer.
|
|
124
|
+
|
|
125
|
+
Returns True if the user confirms, False if cancelled.
|
|
126
|
+
"""
|
|
127
|
+
cli_err(get_string(
|
|
128
|
+
'cli_msg_generate_confirm',
|
|
129
|
+
path=path,
|
|
130
|
+
count=node_count,
|
|
131
|
+
conflict_hint=get_string(
|
|
132
|
+
'cli_confirm_conflict_enabled' if fail_on_conflict
|
|
133
|
+
else 'cli_confirm_conflict_disabled',
|
|
134
|
+
),
|
|
135
|
+
warn_hint=get_string(
|
|
136
|
+
'cli_confirm_warn_count', count=warn_count,
|
|
137
|
+
) if warn_count else get_string('cli_confirm_no_warnings'),
|
|
138
|
+
))
|
|
139
|
+
return read_yes_no(default=False)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def resolve_confirm_mode(args) -> str | None:
|
|
143
|
+
"""
|
|
144
|
+
Resolve the confirmation mode from the arguments: yes / env / interactive / None.
|
|
145
|
+
|
|
146
|
+
Priority: --yes > TREEING_YES > --confirm.
|
|
147
|
+
"""
|
|
148
|
+
if getattr(args, 'yes', False):
|
|
149
|
+
return 'yes'
|
|
150
|
+
if assume_yes_from_env():
|
|
151
|
+
return 'env'
|
|
152
|
+
if getattr(args, 'confirm', False):
|
|
153
|
+
return 'interactive'
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def gate_before_write(
|
|
158
|
+
args,
|
|
159
|
+
*,
|
|
160
|
+
path: str,
|
|
161
|
+
node_count: int,
|
|
162
|
+
fail_on_conflict: bool,
|
|
163
|
+
warn_count: int,
|
|
164
|
+
) -> tuple[int | None, str | None]:
|
|
165
|
+
"""
|
|
166
|
+
Pre-write confirmation entry point.
|
|
167
|
+
|
|
168
|
+
Returns (early_exit_code, confirm_mode). A non-None early_exit_code means
|
|
169
|
+
the run should exit early and skip writing.
|
|
170
|
+
"""
|
|
171
|
+
if args.dry_run:
|
|
172
|
+
if args.confirm:
|
|
173
|
+
warn_confirm_ignored_on_dry_run()
|
|
174
|
+
return None, None
|
|
175
|
+
|
|
176
|
+
mode = resolve_confirm_mode(args)
|
|
177
|
+
if mode in ('yes', 'env'):
|
|
178
|
+
audit_yes_confirmed(path=path, node_count=node_count, quiet=args.quiet)
|
|
179
|
+
return None, mode
|
|
180
|
+
|
|
181
|
+
if not args.confirm:
|
|
182
|
+
return None, None
|
|
183
|
+
|
|
184
|
+
if not is_interactive_tty():
|
|
185
|
+
cli_err(get_string('cli_error_confirm_not_tty'))
|
|
186
|
+
return 1, None
|
|
187
|
+
|
|
188
|
+
if not prompt_generate_confirm(
|
|
189
|
+
path=path,
|
|
190
|
+
node_count=node_count,
|
|
191
|
+
fail_on_conflict=fail_on_conflict,
|
|
192
|
+
warn_count=warn_count,
|
|
193
|
+
):
|
|
194
|
+
cli_err(get_string('cli_confirm_cancelled'))
|
|
195
|
+
return 0, None
|
|
196
|
+
|
|
197
|
+
return None, 'interactive'
|
treeing/cli/help_text.py
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
"""
|
|
2
|
+
treeing/cli/help_text.py
|
|
3
|
+
|
|
4
|
+
Defines the layered CLI help system: short help, full help, topic help, and about.
|
|
5
|
+
Provides `build_parser` and `dispatch_help_argv` as entry points.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
from collections.abc import Callable
|
|
12
|
+
|
|
13
|
+
from .. import __version__
|
|
14
|
+
from ..config import get_string, get_ui_string
|
|
15
|
+
from .io import cli_out
|
|
16
|
+
|
|
17
|
+
# topic name -> (title string key, body string key or None to use gui_tooltip_*)
|
|
18
|
+
TOPIC_SPECS: dict[str, tuple[str, str | None, str | None]] = {
|
|
19
|
+
# name: (title_key, body_key, tooltip_fallback_key)
|
|
20
|
+
'input': ('cli_help_topic_title_input', 'cli_help_topic_input', 'gui_tooltip_input'),
|
|
21
|
+
'output': ('cli_help_topic_title_output', 'cli_help_topic_output', None),
|
|
22
|
+
'encoding': ('cli_help_topic_title_encoding', None, 'gui_tooltip_encoding'),
|
|
23
|
+
'parse': ('cli_help_topic_title_parse', 'cli_help_topic_parse', None),
|
|
24
|
+
'no-fix': ('cli_help_topic_title_no_fix', None, 'gui_tooltip_no_fix'),
|
|
25
|
+
'strict-dirs': ('cli_help_topic_title_strict_dirs', None, 'gui_tooltip_strict_dirs'),
|
|
26
|
+
'indent-unit': ('cli_help_topic_title_indent_unit', None, 'gui_tooltip_indent_unit'),
|
|
27
|
+
'dry-run': ('cli_help_topic_title_dry_run', 'cli_help_topic_dry_run', None),
|
|
28
|
+
'allow-nested': ('cli_help_topic_title_allow_nested', None, 'gui_tooltip_allow_nested'),
|
|
29
|
+
'fail-on-conflict': ('cli_help_topic_title_fail_on_conflict', None, 'gui_tooltip_fail_on_conflict'),
|
|
30
|
+
'rollback-on-error': ('cli_help_topic_title_rollback_on_error', 'cli_help_topic_rollback_on_error', None),
|
|
31
|
+
'format': ('cli_help_topic_title_format', 'cli_help_topic_format', 'gui_tooltip_preview'),
|
|
32
|
+
'json': ('cli_help_topic_title_json', 'cli_help_topic_json', None),
|
|
33
|
+
'automation': ('cli_help_topic_title_automation', 'cli_help_topic_automation', None),
|
|
34
|
+
'windows': ('cli_help_topic_title_windows', 'cli_help_topic_windows', None),
|
|
35
|
+
'warnings': ('cli_help_topic_title_warnings', None, 'gui_tooltip_view_warnings'),
|
|
36
|
+
'strict': ('cli_help_topic_title_strict', 'cli_help_topic_strict', None),
|
|
37
|
+
'confirm': ('cli_help_topic_title_confirm', 'cli_help_topic_confirm', None),
|
|
38
|
+
'about': ('cli_help_topic_title_about', None, None),
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# P3: topic index groups (section_key, [topic names])
|
|
42
|
+
TOPIC_INDEX: list[tuple[str, list[str]]] = [
|
|
43
|
+
('cli_help_topics_section_input', ['input', 'output', 'encoding']),
|
|
44
|
+
('cli_help_topics_section_parse', ['parse', 'no-fix', 'strict-dirs', 'indent-unit']),
|
|
45
|
+
('cli_help_topics_section_generate', [
|
|
46
|
+
'dry-run', 'allow-nested', 'fail-on-conflict', 'rollback-on-error',
|
|
47
|
+
'format', 'strict', 'confirm',
|
|
48
|
+
]),
|
|
49
|
+
('cli_help_topics_section_automation', ['json', 'automation', 'warnings']),
|
|
50
|
+
('cli_help_topics_section_platform', ['windows']),
|
|
51
|
+
('cli_help_topics_section_meta', ['about']),
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
TOPIC_ALIASES: dict[str, str] = {
|
|
55
|
+
'i': 'input',
|
|
56
|
+
'o': 'output',
|
|
57
|
+
'p': 'paste',
|
|
58
|
+
'paste': 'input',
|
|
59
|
+
'nested': 'allow-nested',
|
|
60
|
+
'conflict': 'fail-on-conflict',
|
|
61
|
+
'rollback': 'rollback-on-error',
|
|
62
|
+
'yes': 'confirm',
|
|
63
|
+
'y': 'confirm',
|
|
64
|
+
'topics': 'topics',
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _body_for_topic(name: str) -> str:
|
|
69
|
+
"""
|
|
70
|
+
Fetch the body text for a help topic.
|
|
71
|
+
|
|
72
|
+
Prefer the dedicated body_key; otherwise fall back to the gui_tooltip_*
|
|
73
|
+
text. The `about` topic is special-cased and prints the about text.
|
|
74
|
+
"""
|
|
75
|
+
if name == 'about':
|
|
76
|
+
return get_ui_string('gui_msg_about', version=__version__)
|
|
77
|
+
spec = TOPIC_SPECS.get(name)
|
|
78
|
+
if not spec:
|
|
79
|
+
return ''
|
|
80
|
+
_, body_key, tooltip_key = spec
|
|
81
|
+
if body_key:
|
|
82
|
+
text = get_string(body_key)
|
|
83
|
+
if text != body_key:
|
|
84
|
+
return text
|
|
85
|
+
if tooltip_key:
|
|
86
|
+
return get_string(tooltip_key)
|
|
87
|
+
return ''
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def print_about() -> None:
|
|
91
|
+
"""Print the --about content (shares the same text as the GUI about dialogue)."""
|
|
92
|
+
cli_out(get_ui_string('gui_msg_about', version=__version__))
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def print_topics_index() -> None:
|
|
96
|
+
"""Print the topic index for `treeing help`, grouped by section."""
|
|
97
|
+
cli_out(get_ui_string('cli_help_topics_intro'))
|
|
98
|
+
cli_out('')
|
|
99
|
+
for section_key, names in TOPIC_INDEX:
|
|
100
|
+
cli_out(get_string(section_key))
|
|
101
|
+
for name in names:
|
|
102
|
+
title_key = TOPIC_SPECS[name][0]
|
|
103
|
+
cli_out(get_string('cli_help_topics_item', topic=name, title=get_string(title_key)))
|
|
104
|
+
cli_out('')
|
|
105
|
+
cli_out(get_ui_string('cli_help_topics_footer'))
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def print_topic(name: str) -> int:
|
|
109
|
+
"""
|
|
110
|
+
Print detailed help for a single topic.
|
|
111
|
+
|
|
112
|
+
Supports aliases (e.g. nested -> allow-nested); an unknown topic prints an
|
|
113
|
+
error first, then lists the available topics.
|
|
114
|
+
"""
|
|
115
|
+
canonical = TOPIC_ALIASES.get(name, name)
|
|
116
|
+
if canonical == 'topics':
|
|
117
|
+
print_topics_index()
|
|
118
|
+
return 0
|
|
119
|
+
if canonical not in TOPIC_SPECS:
|
|
120
|
+
cli_out(get_string('cli_help_unknown_topic', topic=name))
|
|
121
|
+
cli_out('')
|
|
122
|
+
print_topics_index()
|
|
123
|
+
return 1
|
|
124
|
+
title = get_string(TOPIC_SPECS[canonical][0])
|
|
125
|
+
body = _body_for_topic(canonical)
|
|
126
|
+
cli_out(title)
|
|
127
|
+
cli_out('')
|
|
128
|
+
cli_out(body)
|
|
129
|
+
cli_out('')
|
|
130
|
+
cli_out(get_ui_string('cli_help_topic_see_also'))
|
|
131
|
+
return 0
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def dispatch_help_argv(argv: list[str]) -> int | None:
|
|
135
|
+
"""
|
|
136
|
+
Handle help / --about / --help-full requests before formal argument parsing.
|
|
137
|
+
|
|
138
|
+
Returns an exit code when handled; returns None when this is not a help
|
|
139
|
+
request and normal flow should continue.
|
|
140
|
+
"""
|
|
141
|
+
if len(argv) == 2 and argv[1] == '--about':
|
|
142
|
+
print_about()
|
|
143
|
+
return 0
|
|
144
|
+
if len(argv) == 2 and argv[1] == '--help-full':
|
|
145
|
+
print_help_full()
|
|
146
|
+
return 0
|
|
147
|
+
try:
|
|
148
|
+
idx = argv.index('help')
|
|
149
|
+
except ValueError:
|
|
150
|
+
return None
|
|
151
|
+
if idx != 1:
|
|
152
|
+
return None
|
|
153
|
+
topic = argv[idx + 1] if len(argv) > idx + 1 else None
|
|
154
|
+
if topic is None:
|
|
155
|
+
print_topics_index()
|
|
156
|
+
return 0
|
|
157
|
+
return print_topic(topic)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _add_group(parser: argparse.ArgumentParser, title_key: str, add_fn: Callable) -> None:
|
|
161
|
+
"""Add an argument group to the parser; the group title comes from the string resources."""
|
|
162
|
+
group = parser.add_argument_group(get_string(title_key))
|
|
163
|
+
add_fn(group)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
167
|
+
"""Build and return the CLI argument parser; all text comes from strings.json."""
|
|
168
|
+
parser = argparse.ArgumentParser(
|
|
169
|
+
description=get_string('cli_help_desc'),
|
|
170
|
+
epilog=get_ui_string('cli_help_epilog'),
|
|
171
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
172
|
+
)
|
|
173
|
+
parser.add_argument(
|
|
174
|
+
'--version', action='version',
|
|
175
|
+
version=get_string('cli_version_fmt', version=__version__),
|
|
176
|
+
)
|
|
177
|
+
parser.add_argument(
|
|
178
|
+
'--about', action='store_true',
|
|
179
|
+
help=get_string('cli_about_flag'),
|
|
180
|
+
)
|
|
181
|
+
parser.add_argument(
|
|
182
|
+
'--help-full', action='store_true',
|
|
183
|
+
help=get_string('cli_help_full_flag'),
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
def add_input(g):
|
|
187
|
+
"""Register input-related arguments: -i/--input, -p/--paste, --encoding."""
|
|
188
|
+
g.add_argument('-i', '--input', help=get_string('cli_input_file'))
|
|
189
|
+
g.add_argument('-p', '--paste', action='store_true', help=get_string('cli_paste'))
|
|
190
|
+
g.add_argument(
|
|
191
|
+
'--encoding', default='utf-8', metavar='ENC',
|
|
192
|
+
help=get_string('cli_encoding'),
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
def add_output(g):
|
|
196
|
+
"""Register output-related arguments: -o/--output, --use-settings, --check-writable."""
|
|
197
|
+
g.add_argument('-o', '--output', default=None, help=get_string('cli_output_dir'))
|
|
198
|
+
g.add_argument(
|
|
199
|
+
'--use-settings', action='store_true',
|
|
200
|
+
help=get_string('cli_use_settings'),
|
|
201
|
+
)
|
|
202
|
+
g.add_argument(
|
|
203
|
+
'--check-writable', action='store_true',
|
|
204
|
+
help=get_string('cli_check_writable'),
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
def add_parse(g):
|
|
208
|
+
"""Register parse-related arguments: --no-fix, --strict-dirs, --indent-unit."""
|
|
209
|
+
g.add_argument('--no-fix', action='store_true', help=get_string('cli_no_fix'))
|
|
210
|
+
g.add_argument(
|
|
211
|
+
'--strict-dirs', action='store_true',
|
|
212
|
+
help=get_string('cli_strict_dirs'),
|
|
213
|
+
)
|
|
214
|
+
g.add_argument(
|
|
215
|
+
'--indent-unit', type=int, default=None, metavar='N',
|
|
216
|
+
help=get_string('cli_indent_unit'),
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
def add_generate(g):
|
|
220
|
+
"""Register generation-related arguments: dry-run, nested names, conflict handling, confirmation mode, etc."""
|
|
221
|
+
g.add_argument('--dry-run', action='store_true', help=get_string('cli_dry_run'))
|
|
222
|
+
g.add_argument(
|
|
223
|
+
'--allow-nested-names', action='store_true',
|
|
224
|
+
help=get_string('cli_allow_nested_names'),
|
|
225
|
+
)
|
|
226
|
+
g.add_argument(
|
|
227
|
+
'--fail-on-conflict', action='store_true',
|
|
228
|
+
help=get_string('cli_fail_on_conflict'),
|
|
229
|
+
)
|
|
230
|
+
g.add_argument(
|
|
231
|
+
'--fail-on-duplicate', action='store_true',
|
|
232
|
+
help=get_string('cli_fail_on_duplicate'),
|
|
233
|
+
)
|
|
234
|
+
g.add_argument(
|
|
235
|
+
'--strict', action='store_true',
|
|
236
|
+
help=get_string('cli_strict'),
|
|
237
|
+
)
|
|
238
|
+
g.add_argument(
|
|
239
|
+
'--rollback-on-error', action='store_true',
|
|
240
|
+
help=get_string('cli_rollback_on_error'),
|
|
241
|
+
)
|
|
242
|
+
confirm = g.add_mutually_exclusive_group()
|
|
243
|
+
confirm.add_argument(
|
|
244
|
+
'--confirm', action='store_true',
|
|
245
|
+
help=get_string('cli_confirm'),
|
|
246
|
+
)
|
|
247
|
+
confirm.add_argument(
|
|
248
|
+
'-y', '--yes', action='store_true',
|
|
249
|
+
help=get_string('cli_yes'),
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
def add_automation(g):
|
|
253
|
+
"""Register automation-related arguments: --json, --quiet, --warn-exit-code, --warnings-file, --format, warning cap."""
|
|
254
|
+
g.add_argument('--json', action='store_true', help=get_string('cli_json'))
|
|
255
|
+
g.add_argument('--quiet', action='store_true', help=get_string('cli_quiet'))
|
|
256
|
+
g.add_argument(
|
|
257
|
+
'--warn-exit-code', action='store_true',
|
|
258
|
+
help=get_string('cli_warn_exit_code'),
|
|
259
|
+
)
|
|
260
|
+
g.add_argument(
|
|
261
|
+
'--warnings-file', metavar='PATH',
|
|
262
|
+
help=get_string('cli_warnings_file'),
|
|
263
|
+
)
|
|
264
|
+
g.add_argument(
|
|
265
|
+
'--format', choices=['text', 'tree'], default='text',
|
|
266
|
+
help=get_string('cli_format'),
|
|
267
|
+
)
|
|
268
|
+
warn_limit = g.add_mutually_exclusive_group()
|
|
269
|
+
warn_limit.add_argument(
|
|
270
|
+
'--no-warn-limit', action='store_true',
|
|
271
|
+
help=get_string('cli_no_warn_limit'),
|
|
272
|
+
)
|
|
273
|
+
warn_limit.add_argument(
|
|
274
|
+
'--warn-limit', type=int, default=None, metavar='N',
|
|
275
|
+
help=get_string('cli_warn_limit'),
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
_add_group(parser, 'cli_help_group_input', add_input)
|
|
279
|
+
_add_group(parser, 'cli_help_group_output', add_output)
|
|
280
|
+
_add_group(parser, 'cli_help_group_parse', add_parse)
|
|
281
|
+
_add_group(parser, 'cli_help_group_generate', add_generate)
|
|
282
|
+
_add_group(parser, 'cli_help_group_automation', add_automation)
|
|
283
|
+
return parser
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def print_help_full() -> None:
|
|
287
|
+
"""Full help: short help plus an expanded description for each option."""
|
|
288
|
+
parser = build_parser()
|
|
289
|
+
parser.print_help()
|
|
290
|
+
cli_out('')
|
|
291
|
+
cli_out(get_string('cli_help_full_header'))
|
|
292
|
+
cli_out('')
|
|
293
|
+
sections = [
|
|
294
|
+
('cli_help_full_section_input', ['input', 'output', 'encoding']),
|
|
295
|
+
('cli_help_full_section_parse', ['parse', 'no-fix', 'strict-dirs', 'indent-unit']),
|
|
296
|
+
('cli_help_full_section_generate', [
|
|
297
|
+
'dry-run', 'allow-nested', 'fail-on-conflict', 'rollback-on-error', 'format', 'strict', 'confirm',
|
|
298
|
+
]),
|
|
299
|
+
('cli_help_full_section_automation', ['json', 'automation', 'warnings']),
|
|
300
|
+
('cli_help_full_section_platform', ['windows']),
|
|
301
|
+
]
|
|
302
|
+
for section_key, topics in sections:
|
|
303
|
+
cli_out(get_string(section_key))
|
|
304
|
+
for topic in topics:
|
|
305
|
+
title = get_string(TOPIC_SPECS[topic][0])
|
|
306
|
+
body = _body_for_topic(topic)
|
|
307
|
+
cli_out(get_string('cli_help_full_topic_line', topic=topic, title=title))
|
|
308
|
+
for line in body.splitlines():
|
|
309
|
+
cli_out(f' {line}')
|
|
310
|
+
cli_out('')
|
|
311
|
+
cli_out(get_ui_string('cli_help_topics_footer'))
|
treeing/cli/io.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""
|
|
2
|
+
treeing/cli/io.py
|
|
3
|
+
|
|
4
|
+
Defines CLI standard-stream configuration and safe writing.
|
|
5
|
+
Provides `configure_stdio` (UTF-8 + replace) and the cli_out / cli_warn /
|
|
6
|
+
cli_err helpers, so Windows legacy-codepage consoles do not crash on output.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
_configured = False
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def configure_stdio() -> None:
|
|
15
|
+
"""
|
|
16
|
+
At startup, set stdout/stderr to UTF-8 + replace mode.
|
|
17
|
+
|
|
18
|
+
Runs once; later calls return immediately.
|
|
19
|
+
"""
|
|
20
|
+
global _configured
|
|
21
|
+
if _configured:
|
|
22
|
+
return
|
|
23
|
+
for stream in (sys.stdout, sys.stderr):
|
|
24
|
+
if hasattr(stream, 'reconfigure'):
|
|
25
|
+
try:
|
|
26
|
+
stream.reconfigure(encoding='utf-8', errors='replace')
|
|
27
|
+
except (OSError, ValueError, AttributeError):
|
|
28
|
+
pass
|
|
29
|
+
_configured = True
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _safe_write(stream, text: str, *, end: str = '\n') -> None:
|
|
33
|
+
"""
|
|
34
|
+
Safe write: try a direct write first, then fall back to buffer + replace encoding.
|
|
35
|
+
"""
|
|
36
|
+
msg = text + end
|
|
37
|
+
try:
|
|
38
|
+
stream.write(msg)
|
|
39
|
+
stream.flush()
|
|
40
|
+
except UnicodeEncodeError:
|
|
41
|
+
encoding = getattr(stream, 'encoding', None) or 'utf-8'
|
|
42
|
+
if hasattr(stream, 'buffer'):
|
|
43
|
+
stream.buffer.write(msg.encode(encoding, errors='replace'))
|
|
44
|
+
stream.buffer.flush()
|
|
45
|
+
else:
|
|
46
|
+
raise
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def cli_out(text: str, *, end: str = '\n') -> None:
|
|
50
|
+
"""
|
|
51
|
+
Success summaries and [WARN] warnings go to stdout.
|
|
52
|
+
|
|
53
|
+
Easier for pipes / agents to parse, and friendlier under PowerShell.
|
|
54
|
+
"""
|
|
55
|
+
_safe_write(sys.stdout, text, end=end)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def cli_warn(text: str, *, end: str = '\n') -> None:
|
|
59
|
+
"""
|
|
60
|
+
Warning prompts go to stdout (same stream as cli_out).
|
|
61
|
+
|
|
62
|
+
MING routes warnings through stdout so agents / scripts get stable
|
|
63
|
+
structured output via a pipe, leaving genuine errors for stderr.
|
|
64
|
+
"""
|
|
65
|
+
_safe_write(sys.stdout, text, end=end)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def cli_err(text: str, *, end: str = '\n') -> None:
|
|
69
|
+
"""
|
|
70
|
+
Errors and interactive prompts go to stderr ([ERR] and confirm prompts).
|
|
71
|
+
"""
|
|
72
|
+
_safe_write(sys.stderr, text, end=end)
|