mfgparams 2.5.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.
Files changed (67) hide show
  1. mfgparams/__init__.py +72 -0
  2. mfgparams/__main__.py +386 -0
  3. mfgparams/config.py +112 -0
  4. mfgparams/console/__init__.py +19 -0
  5. mfgparams/console/__main__.py +22 -0
  6. mfgparams/console/cli.py +97 -0
  7. mfgparams/console/i18n.py +136 -0
  8. mfgparams/console/locales/__init__.py +26 -0
  9. mfgparams/console/locales/en.py +203 -0
  10. mfgparams/console/tui/__init__.py +9 -0
  11. mfgparams/console/tui/app.py +1540 -0
  12. mfgparams/console/tui/forms.py +245 -0
  13. mfgparams/console/tui/machining_menu.py +102 -0
  14. mfgparams/console/tui/material_picker.py +330 -0
  15. mfgparams/console/tui/menu.py +110 -0
  16. mfgparams/console/tui/screens/__init__.py +3 -0
  17. mfgparams/console/tui/screens/about.py +24 -0
  18. mfgparams/console/tui/screens/configuration.py +118 -0
  19. mfgparams/console/tui/screens/drilling.py +298 -0
  20. mfgparams/console/tui/screens/help.py +19 -0
  21. mfgparams/console/tui/screens/milling.py +468 -0
  22. mfgparams/console/tui/screens/split_pane.py +476 -0
  23. mfgparams/console/tui/screens/turning.py +340 -0
  24. mfgparams/console/tui/terminal_capability.py +65 -0
  25. mfgparams/data/__init__.py +8 -0
  26. mfgparams/data/materials.toml +175 -0
  27. mfgparams/i18n.py +151 -0
  28. mfgparams/locales/__init__.py +14 -0
  29. mfgparams/locales/en.py +161 -0
  30. mfgparams/logging_setup.py +34 -0
  31. mfgparams/models.py +283 -0
  32. mfgparams/processes/__init__.py +21 -0
  33. mfgparams/processes/machining/__init__.py +20 -0
  34. mfgparams/processes/machining/drilling/__init__.py +524 -0
  35. mfgparams/processes/machining/drilling/data/__init__.py +8 -0
  36. mfgparams/processes/machining/drilling/data/tools.toml +28 -0
  37. mfgparams/processes/machining/drilling/formulas.py +175 -0
  38. mfgparams/processes/machining/drilling/tools.py +152 -0
  39. mfgparams/processes/machining/milling/__init__.py +13 -0
  40. mfgparams/processes/machining/milling/_calculate.py +638 -0
  41. mfgparams/processes/machining/milling/_shared.py +342 -0
  42. mfgparams/processes/machining/milling/_tool_registry.py +151 -0
  43. mfgparams/processes/machining/milling/end_milling/__init__.py +183 -0
  44. mfgparams/processes/machining/milling/end_milling/data/__init__.py +8 -0
  45. mfgparams/processes/machining/milling/end_milling/data/tools.toml +40 -0
  46. mfgparams/processes/machining/milling/end_milling/formulas.py +154 -0
  47. mfgparams/processes/machining/milling/end_milling/tools.py +71 -0
  48. mfgparams/processes/machining/milling/face_milling/__init__.py +186 -0
  49. mfgparams/processes/machining/milling/face_milling/data/__init__.py +8 -0
  50. mfgparams/processes/machining/milling/face_milling/data/tools.toml +36 -0
  51. mfgparams/processes/machining/milling/face_milling/formulas.py +156 -0
  52. mfgparams/processes/machining/milling/face_milling/tools.py +68 -0
  53. mfgparams/processes/machining/turning/__init__.py +779 -0
  54. mfgparams/processes/machining/turning/data/__init__.py +1 -0
  55. mfgparams/processes/machining/turning/data/tools.toml +32 -0
  56. mfgparams/processes/machining/turning/formulas.py +497 -0
  57. mfgparams/processes/machining/turning/tools.py +143 -0
  58. mfgparams/registry.py +473 -0
  59. mfgparams/registry_config.py +450 -0
  60. mfgparams/units.py +197 -0
  61. mfgparams/validation.py +718 -0
  62. mfgparams-2.5.0.dist-info/METADATA +703 -0
  63. mfgparams-2.5.0.dist-info/RECORD +67 -0
  64. mfgparams-2.5.0.dist-info/WHEEL +5 -0
  65. mfgparams-2.5.0.dist-info/entry_points.txt +2 -0
  66. mfgparams-2.5.0.dist-info/licenses/LICENSE.md +89 -0
  67. mfgparams-2.5.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,97 @@
1
+ """Console entry point: launches the text GUI, or exits with a clear message
2
+ if this terminal cannot support it (specs/017-console-text-gui).
3
+
4
+ The REPL this module used to hold is deleted entirely (FR-001, FR-007) --
5
+ not hidden behind a flag, not kept for scripting. See spec.md's "Entry-point
6
+ precedence" note in Context for why, and contracts/console-tui-contract.md
7
+ §1 for the full entry-point contract.
8
+
9
+ `main()` here is the FR-006/User Story 3 negative-path implementation
10
+ itself, not a stub deferred elsewhere (/speckit-analyze finding F2): it
11
+ calls `terminal_capability.check()` (research.md #2) *before* importing or
12
+ constructing anything prompt-toolkit-related, and exits via the catalog
13
+ message on failure.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import argparse
19
+ import sys
20
+
21
+ from mfgparams.console.tui import terminal_capability
22
+ from mfgparams.i18n import get_locale, translate
23
+ from mfgparams.logging_setup import configure_logging
24
+
25
+
26
+ def _report_unsupported_terminal(capability: terminal_capability.TerminalCapability) -> int:
27
+ """Print the FR-006 message and return the exit status."""
28
+
29
+ locale = get_locale()
30
+ if not capability.has_tty:
31
+ reason = translate(locale, "console.tui_unavailable.reason.no_tty")
32
+ else:
33
+ reason = translate(
34
+ locale,
35
+ "console.tui_unavailable.reason.too_small",
36
+ columns=capability.columns,
37
+ lines=capability.lines,
38
+ min_columns=terminal_capability.MIN_COLUMNS,
39
+ min_lines=terminal_capability.MIN_LINES,
40
+ )
41
+ message = translate(
42
+ locale,
43
+ "console.tui_unavailable",
44
+ reason=reason,
45
+ min_columns=terminal_capability.MIN_COLUMNS,
46
+ min_lines=terminal_capability.MIN_LINES,
47
+ )
48
+ print(message, file=sys.stderr)
49
+ return 1
50
+
51
+
52
+ def _parse_args(argv: list[str] | None = None) -> argparse.Namespace:
53
+ """Parse CLI arguments (currently just ``--materials-config``)."""
54
+
55
+ parser = argparse.ArgumentParser(prog="mfgparams")
56
+ parser.add_argument(
57
+ "--materials-config",
58
+ dest="materials_config",
59
+ default=None,
60
+ metavar="PATH",
61
+ help=(
62
+ "Optional path to a TOML file adding/overriding materials and "
63
+ "drilling tools (see contracts/materials-config-schema.md)."
64
+ ),
65
+ )
66
+ return parser.parse_args(argv)
67
+
68
+
69
+ def main() -> int:
70
+ """Console-script entry point (``mfgparams`` / ``python -m mfgparams``).
71
+
72
+ Returns the process exit status: ``1`` if this terminal cannot support
73
+ the text GUI (FR-006), otherwise whatever the text GUI returns once it
74
+ has run to completion (``0`` when it returns nothing).
75
+
76
+ Argument parsing runs *before* the terminal-capability gate, not after:
77
+ ``--help``/``-h`` (and an invalid argument) must work over a pipe like
78
+ any ordinary CLI tool's does -- `argparse` exits on its own for both
79
+ before this function would otherwise reach the gate, and gating first
80
+ would incorrectly block a `--help` request piped through `| less` or a
81
+ CI script with the FR-006 message instead of the help text.
82
+ """
83
+
84
+ configure_logging()
85
+ args = _parse_args()
86
+
87
+ capability = terminal_capability.check()
88
+ if not capability.supported:
89
+ return _report_unsupported_terminal(capability)
90
+
91
+ from mfgparams.console.tui.app import run
92
+
93
+ try:
94
+ run(materials_config_path=args.materials_config)
95
+ except (KeyboardInterrupt, EOFError):
96
+ print()
97
+ return 0
@@ -0,0 +1,136 @@
1
+ """Message catalog loader and locale resolution for the console.
2
+
3
+ Mirrors :mod:`mfgparams.i18n` exactly in structure and contract, scoped to
4
+ :mod:`mfgparams.console.locales` instead of :mod:`mfgparams.locales`
5
+ (specs/015-console-i18n-relocation research.md #1 — a deliberate duplicate,
6
+ not a shared module, because the two catalogs have different compatibility
7
+ contracts: core's is public-API surface, console's is presentation and can
8
+ change freely).
9
+
10
+ Provides:
11
+
12
+ - :func:`get_locale`: reads ``MFGPARAMS_LOCALE``, resolved against the
13
+ console's own bundled catalogs. Callers that need a *fixed-for-session*
14
+ locale (the console itself) MUST call this exactly once at startup and
15
+ hold the result for the lifetime of that session.
16
+ - :func:`translate`: looks up a message by ID for a given locale, falling
17
+ back to the console's English catalog for any missing locale or key, and
18
+ substituting ``str.format()``-style named placeholders. Never raises to
19
+ the caller — a missing placeholder value is logged (in English) rather
20
+ than surfaced as an exception.
21
+
22
+ This module intentionally contains no user-facing English text itself; all
23
+ strings live in ``mfgparams.console.locales.*`` catalog modules.
24
+
25
+ Does **not** duplicate :func:`mfgparams.i18n.get_raw_locale`: that function
26
+ has no catalog dependency (it resolves ``MFGPARAMS_LOCALE`` verbatim, for
27
+ data-driven lookups such as ``WorkpieceMaterial.translations``) and stays a
28
+ ``mfgparams.i18n`` import wherever it is used — those lookups are Story 3 of
29
+ specs/015-console-i18n-relocation and are untouched by this catalog split.
30
+ """
31
+
32
+ from __future__ import annotations
33
+
34
+ import importlib
35
+ import logging
36
+ import os
37
+
38
+ DEFAULT_LOCALE = "en"
39
+ _LOCALE_ENV_VAR = "MFGPARAMS_LOCALE"
40
+
41
+ _logger = logging.getLogger(__name__)
42
+
43
+ # Cache of locale -> catalog dict (or None if the locale has no bundled
44
+ # module). A plain dict (rather than functools.lru_cache) so tests can
45
+ # register/clear fixture catalogs deterministically.
46
+ _catalog_cache: dict[str, dict[str, str] | None] = {}
47
+
48
+
49
+ def _load_catalog(locale: str) -> dict[str, str] | None:
50
+ """Load the ``MESSAGES`` dict from ``mfgparams.console.locales.<locale>``.
51
+
52
+ Returns ``None`` if no such bundled locale module exists. Results are
53
+ cached per locale for the process lifetime.
54
+ """
55
+
56
+ if locale in _catalog_cache:
57
+ return _catalog_cache[locale]
58
+
59
+ try:
60
+ module = importlib.import_module(f"mfgparams.console.locales.{locale}")
61
+ except ImportError:
62
+ _catalog_cache[locale] = None
63
+ return None
64
+
65
+ catalog = getattr(module, "MESSAGES", None)
66
+ _catalog_cache[locale] = catalog
67
+ return catalog
68
+
69
+
70
+ def clear_catalog_cache() -> None:
71
+ """Clear the locale catalog cache (test support only)."""
72
+
73
+ _catalog_cache.clear()
74
+
75
+
76
+ def get_locale() -> str:
77
+ """Resolve the active locale from ``MFGPARAMS_LOCALE``, against the
78
+ console's own bundled catalogs.
79
+
80
+ Returns the environment variable's value if it is non-empty and matches
81
+ a bundled locale module; otherwise returns :data:`DEFAULT_LOCALE`
82
+ (``"en"``). No OS/system locale (``LANG``/``LC_ALL``) auto-detection is
83
+ performed. The console MUST call this function exactly once per session
84
+ and hold the result, exactly as :func:`mfgparams.i18n.get_locale`
85
+ requires of its own callers.
86
+ """
87
+
88
+ raw = os.environ.get(_LOCALE_ENV_VAR, "")
89
+ if raw and _load_catalog(raw) is not None:
90
+ return raw
91
+ return DEFAULT_LOCALE
92
+
93
+
94
+ def has_message(locale: str, key: str) -> bool:
95
+ """Report whether ``key`` resolves to a real catalog entry.
96
+
97
+ Uses the same lookup order as :func:`translate` (``locale`` first, then
98
+ the English catalog) but stops before formatting. See
99
+ :func:`mfgparams.i18n.has_message`'s docstring for why this is needed
100
+ rather than comparing ``translate``'s result against the key.
101
+ """
102
+
103
+ catalog = _load_catalog(locale)
104
+ if catalog is not None and key in catalog:
105
+ return True
106
+ english = _load_catalog(DEFAULT_LOCALE) or {}
107
+ return key in english
108
+
109
+
110
+ def translate(locale: str, key: str, **kwargs: object) -> str:
111
+ """Look up and format message ``key`` for ``locale``.
112
+
113
+ Falls back to the console's English catalog if ``locale`` has no
114
+ bundled module or lacks ``key``. If a placeholder required by the
115
+ message template is missing from ``kwargs``, this function does NOT
116
+ raise; instead it logs a warning (in English) and returns the
117
+ unformatted template as a usable fallback.
118
+ """
119
+
120
+ catalog = _load_catalog(locale)
121
+ template = catalog.get(key) if catalog else None
122
+
123
+ if template is None:
124
+ english = _load_catalog(DEFAULT_LOCALE) or {}
125
+ template = english.get(key, key)
126
+
127
+ try:
128
+ return template.format(**kwargs)
129
+ except (KeyError, IndexError, ValueError):
130
+ _logger.warning(
131
+ "i18n: missing or invalid placeholder value for message key %r "
132
+ "(locale=%r); returning unformatted template",
133
+ key,
134
+ locale,
135
+ )
136
+ return template
@@ -0,0 +1,26 @@
1
+ """Message catalogs owned by the console, one pure-Python module per locale.
2
+
3
+ Mirrors :mod:`mfgparams.locales`'s shape exactly (each sibling module exposes
4
+ a module-level ``MESSAGES: dict[str, str]``), but a separate package: this
5
+ catalog holds UI strings (prompts, labels, status/progress text) the console
6
+ displays, while :mod:`mfgparams.locales` keeps only the entries that must
7
+ remain reachable without the ``console`` extra installed — error text
8
+ (``error.*``/``warning.*``/``notice.*``) and the console-unavailable message
9
+ (``console.missing_dependency*``) — per
10
+ specs/015-console-i18n-relocation/contracts/catalogue-ownership-contract.md.
11
+
12
+ A key lives in exactly one of the two catalogs — with two documented
13
+ exceptions (FR-004): ``console.missing_dependency*`` stays core-only, and
14
+ ``cli.label.axial_depth_of_cut``/``radial_depth_of_cut``/``width_of_cut``
15
+ live in **both**, because core embeds one of them inside its own English
16
+ error text (found during T007; see
17
+ specs/015-console-i18n-relocation/contracts/catalogue-ownership-contract.md
18
+ for the full reasoning on both exceptions). Outside those two, moving an
19
+ entry here removes it from :mod:`mfgparams.locales` in the same change.
20
+
21
+ This feature ships only :mod:`mfgparams.console.locales.en`; additional
22
+ languages can be added later by contributing a new sibling module of the
23
+ same shape, without any code changes elsewhere.
24
+ """
25
+
26
+ from __future__ import annotations
@@ -0,0 +1,203 @@
1
+ """English (``en``) message catalog for the console — the default and fallback locale.
2
+
3
+ Message IDs are stable and language-independent; do not rename an existing
4
+ key when editing its English text (other locale modules and tests key off
5
+ these IDs). See ``specs/015-console-i18n-relocation/data-model.md`` for the
6
+ entity definition this module implements, and
7
+ ``specs/017-console-text-gui/contracts/console-tui-contract.md`` §4 for which
8
+ strings belong here versus in :mod:`mfgparams.locales.en`.
9
+
10
+ specs/017-console-text-gui replaces the REPL with a full-screen text GUI: the
11
+ ``cli.*`` keys this catalog held for the REPL are retired along with the REPL
12
+ code that used them, replaced by the ``tui.*`` keys below (contracts §4 --
13
+ new keys introduced by that feature are namespaced ``tui.*``).
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ MESSAGES: dict[str, str] = {
19
+ # --- Top-level menu and Machining submenu (FR-009, FR-010) ---
20
+ "tui.menu.title": "mfgparams",
21
+ "tui.menu.hint": "Arrow keys or the highlighted letter to choose, Enter to select.",
22
+ "tui.menu.machining": "Machining",
23
+ "tui.menu.configuration": "Configuration",
24
+ "tui.menu.about": "About",
25
+ "tui.menu.help": "Help",
26
+ # 018-tui-splitpane-redesign FR-001: new to the persistent bar -- 017 had
27
+ # no labeled Exit item, only an unlabeled Escape/Ctrl-Q handler.
28
+ "tui.menu.exit": "Exit",
29
+ # Exit confirmation dialog (revision, per direct user feedback): shown
30
+ # as a floating Yes/No prompt when Exit is selected from the bar,
31
+ # rather than exiting immediately.
32
+ "tui.exit_confirm.message": "Are you sure you want to exit?",
33
+ "tui.exit_confirm.yes": "Yes",
34
+ "tui.exit_confirm.no": "No",
35
+ "tui.machining_menu.title": "Machining",
36
+ "tui.machining_menu.milling": "Milling",
37
+ "tui.machining_menu.drilling": "Drilling",
38
+ "tui.machining_menu.turning": "Turning",
39
+ # --- Shared dialog chrome ---
40
+ "tui.action.ok": "OK",
41
+ "tui.action.back": "Back",
42
+ "tui.error.title": "Invalid input",
43
+ # --- Unit system / calculation mode (shared by Milling and Drilling) ---
44
+ "tui.label.unit_system": "Unit system",
45
+ "tui.unit_system.metric": "metric",
46
+ "tui.unit_system.imperial": "imperial",
47
+ "tui.label.mode": "Calculation mode",
48
+ "tui.mode.standard": "standard",
49
+ "tui.mode.power_constrained": "power-constrained",
50
+ "tui.mode.fixed_rpm": "fixed-rpm",
51
+ "tui.mode.feed_rate_constrained": "feed-rate-constrained",
52
+ "tui.mode.rotation_and_feed_constrained": "rotation-and-feed-constrained",
53
+ "tui.mode.power_and_feed_constrained": "power-and-feed-constrained",
54
+ "tui.label.power": "Available power",
55
+ "tui.label.power_required": "Available power",
56
+ "tui.prompt.power_required.invalid": (
57
+ "Please enter a positive numeric value for available power."
58
+ ),
59
+ "tui.label.target_rpm": "Target spindle speed",
60
+ "tui.label.target_feed_rate": "Feed rate per rotation",
61
+ "tui.prompt.target_rpm.invalid": (
62
+ "Please enter a positive numeric value for target spindle speed."
63
+ ),
64
+ # --- Material / tool selection (shared by Milling and Drilling) ---
65
+ "tui.label.material_type": "Material type",
66
+ "material_type.metal": "Metal",
67
+ "material_type.wood": "Wood",
68
+ "material_type.uncategorized": "Uncategorized",
69
+ "tui.label.material": "Material",
70
+ "tui.label.tool": "Drilling tool",
71
+ "tui.label.unit_system_suffix": "{name} [{unit_system}]",
72
+ # --- Material selection window (specs/023-material-selector-dialog) ---
73
+ "tui.material_picker.title": "Select material",
74
+ "tui.material_picker.column_common": "Name",
75
+ "tui.material_picker.column_number": "Material No.",
76
+ "tui.material_picker.column_short": "Short",
77
+ "tui.material_picker.empty": "No matching materials.",
78
+ "tui.material_picker.pane_hint": ("↑↓ move ←→/Space change ↵ detailed search Esc back"),
79
+ # --- Drilling form (FR-002) ---
80
+ "tui.drilling.title": "Drilling",
81
+ "tui.label.diameter": "Drill diameter",
82
+ "tui.label.depth": "Hole depth",
83
+ # 018-tui-splitpane-redesign FR-006 (revision): the right pane's
84
+ # placeholder while required inputs are incomplete -- operation-specific
85
+ # wording, matching the prototype's own `_last_result_text` default.
86
+ "tui.drilling.placeholder": "Enter a diameter and a hole depth to see a result.",
87
+ # --- Turning form (specs/019-turning-calculations) ---
88
+ "tui.turning.title": "Turning",
89
+ "tui.label.workpiece_diameter": "Workpiece diameter",
90
+ "tui.label.depth_of_cut": "Depth of cut",
91
+ "tui.label.turning_tool": "Turning tool",
92
+ "tui.turning.placeholder": (
93
+ "Enter a workpiece diameter, depth of cut, and length of cut to see a result."
94
+ ),
95
+ # --- Milling form (FR-002) ---
96
+ "tui.milling.title": "Milling",
97
+ "tui.milling.placeholder": (
98
+ "Enter diameter, depth of cut, engagement, feed, teeth, and length of cut to see a result."
99
+ ),
100
+ "tui.label.milling_sub_operation": "Milling operation",
101
+ "tui.milling_sub_operation.end_milling": "end milling",
102
+ "tui.milling_sub_operation.face_milling": "face milling",
103
+ "tui.label.mill_diameter": "Cutter diameter",
104
+ "tui.label.end_mill_tool": "End-mill tool",
105
+ "tui.label.face_mill_tool": "Face-mill tool",
106
+ # These three are deliberately NOT tui.*-namespaced (contracts §4's
107
+ # dual-presence exception, unchanged from the REPL): core's
108
+ # validate_depth_of_cut_mm/validate_engagement_mm embed a label inside
109
+ # English error text by this exact key name (mfgparams.locales.en carries
110
+ # the same three, untouched by this feature), and this module's own
111
+ # error-rendering must resolve the identical key to re-translate it. The
112
+ # TUI's own field labels reuse them rather than inventing tui.* aliases,
113
+ # so there is exactly one key per concept, not two that could drift.
114
+ "cli.label.axial_depth_of_cut": "Axial depth of cut",
115
+ "cli.label.radial_depth_of_cut": "Radial depth of cut",
116
+ "cli.label.width_of_cut": "Width of cut",
117
+ "tui.label.feed_per_tooth": "Feed per tooth",
118
+ "tui.label.number_of_teeth": "Number of teeth",
119
+ "tui.label.length_of_cut": "Length of cut",
120
+ "tui.unit.teeth": "teeth",
121
+ # --- Numeric input prompt/validation (shared) ---
122
+ "tui.prompt.number": "{label} ({unit})",
123
+ "tui.prompt.number.with_default": "{label} ({unit}, default {default})",
124
+ "tui.prompt.power.optional_hint": "Leave blank if unknown.",
125
+ # 018-tui-splitpane-redesign, revision (matching the pre-plan prototype
126
+ # exactly): FR-006b's unparseable-number message now surfaces in the
127
+ # bottom status bar, only once the user tries to navigate away from the
128
+ # offending field -- not proactively while still typing (split_pane.py's
129
+ # `_commit_current`).
130
+ "tui.validation.unparseable_number": "'{text}' is not a number -- kept previous value.",
131
+ # The left pane's own title, distinct from the floating window's own
132
+ # title (the operation name -- FR-004, revision) -- matches the
133
+ # prototype's `render_left`.
134
+ "tui.pane.inputs": "Inputs",
135
+ # The bottom status/hint row shown whenever no `OperationScreen.status`
136
+ # message is pending (revision, matching the prototype's
137
+ # `render_bottom`) -- adapted from its literal "Esc/^Q quit" for this
138
+ # app's real semantics: Escape here returns focus to the persistent
139
+ # menu bar, not necessarily exiting the whole application.
140
+ "tui.pane.hint": "↑↓ move ←→/Space change Esc back",
141
+ # --- Result display ---
142
+ # Reused unchanged as the right pane's title (018-tui-splitpane-redesign
143
+ # FR-006/FR-006a) -- "Result" reads correctly whether it labels a modal
144
+ # dialog (017) or a persistent pane (018).
145
+ "tui.result.title": "Result",
146
+ "tui.result.spindle_speed": "Spindle speed: {value} RPM{mode_suffix}",
147
+ "tui.result.spindle_speed.mode_suffix": " ({label})",
148
+ "tui.result.spindle_speed.mode.standard": "recommended",
149
+ "tui.result.spindle_speed.mode.power_constrained": "adjusted to fit available power",
150
+ "tui.result.spindle_speed.mode.fixed_rpm": "user-specified",
151
+ "tui.result.spindle_speed.mode.feed_rate_constrained": "derived from cutting speed",
152
+ "tui.result.feed_rate": "Feed rate: {value} {unit}",
153
+ "tui.result.feed_per_rotation": "Feed per rotation: {value} {unit}",
154
+ "tui.result.machining_time": "Machining time: {value} min",
155
+ "tui.result.torque": "Torque: {value} {unit}",
156
+ "tui.result.power_required": "Power required: {value} {unit}",
157
+ "tui.result.material_removal_rate": "Material removal: {value} {unit}",
158
+ "tui.result.cutting_force": "Cutting force: {value} {unit}",
159
+ "tui.result.warning": "\nWarning: {message}",
160
+ "tui.result.error.title": "Calculation error",
161
+ # --- Configuration screen (read-only, research.md #4) ---
162
+ "tui.configuration.title": "Configuration",
163
+ "tui.configuration.path_label.set": "Configuration file: {path}",
164
+ "tui.configuration.path_label.default": "Configuration file: (bundled defaults)",
165
+ "tui.configuration.section.material_types": "Material types: {items}",
166
+ "tui.configuration.section.materials": "Materials ({material_type}): {items}",
167
+ "tui.configuration.section.tools": "Drilling tools: {items}",
168
+ # 018-tui-splitpane-redesign FR-015: closes the gap PR #94's review found
169
+ # (Configuration only loaded the drilling registry) -- same screen being
170
+ # rebuilt for this feature, don't patch it twice.
171
+ "tui.configuration.section.end_mill_tools": "End-mill tools: {items}",
172
+ "tui.configuration.section.face_mill_tools": "Face-mill tools: {items}",
173
+ # specs/019-turning-calculations: same completeness requirement PR #94's
174
+ # review found for milling -- every tool registry must be listed here.
175
+ "tui.configuration.section.turning_tools": "Turning tools: {items}",
176
+ # --- About screen ---
177
+ "tui.about.title": "About",
178
+ "tui.about.text": (
179
+ "{name} version {version}\n\nLicensed under the PolyForm Noncommercial License 1.0.0.\n"
180
+ "See LICENSE.md for details."
181
+ ),
182
+ # --- Help screen (placeholder, FR-009) ---
183
+ "tui.help.title": "Help",
184
+ "tui.help.text": (
185
+ "Use the arrow keys and Enter to navigate, or the highlighted letter shown "
186
+ "on a menu item to jump to it directly. Press Tab to move between a screen's "
187
+ "fields and its buttons. Choose Back to return to the previous screen."
188
+ ),
189
+ }
190
+
191
+ # specs/021-turning-combined-constraints research.md #8: these two modes'
192
+ # spindle-speed labels genuinely reuse an existing mode's value (the
193
+ # spindle speed's own derivation nature is identical, even though the feed
194
+ # value's source differs) -- assigned by reference to the existing key
195
+ # here, rather than duplicated as a second literal string inside the dict
196
+ # above, so a future wording change to either source value can't silently
197
+ # leave these two out of sync (Copilot review finding on PR #102).
198
+ MESSAGES["tui.result.spindle_speed.mode.rotation_and_feed_constrained"] = MESSAGES[
199
+ "tui.result.spindle_speed.mode.fixed_rpm"
200
+ ]
201
+ MESSAGES["tui.result.spindle_speed.mode.power_and_feed_constrained"] = MESSAGES[
202
+ "tui.result.spindle_speed.mode.power_constrained"
203
+ ]
@@ -0,0 +1,9 @@
1
+ """Full-screen text GUI for ``mfgparams.console`` (specs/017-console-text-gui).
2
+
3
+ Replaces the line-based REPL as the sole interactive entry point. Built on
4
+ prompt-toolkit (confirmed by that feature's technical spike). Contains no
5
+ calculation logic of its own -- every result comes from the public
6
+ ``mfgparams`` calculation API, exactly as the REPL it replaces did.
7
+ """
8
+
9
+ from __future__ import annotations