cleanup: remove references to simple-term-menu
we migrated away long ago. clean up all docs references the dependency itself
This commit is contained in:
parent
1362ffc7d2
commit
37e46c774c
|
|
@ -1260,12 +1260,8 @@ Use `get_hermes_home()` from `hermes_constants` for code paths. Use `display_her
|
|||
for user-facing print/log messages. Hardcoding `~/.hermes` breaks profiles — each profile
|
||||
has its own `HERMES_HOME` directory. This was the source of 5 bugs fixed in PR #3575.
|
||||
|
||||
### DO NOT introduce new `simple_term_menu` usage
|
||||
Existing call sites in `hermes_cli/main.py` remain for legacy fallback only;
|
||||
the preferred UI is curses (stdlib) because `simple_term_menu` has
|
||||
ghost-duplication rendering bugs in tmux/iTerm2 with arrow keys. New
|
||||
interactive menus must use `hermes_cli/curses_ui.py` — see
|
||||
`hermes_cli/tools_config.py` for the canonical pattern.
|
||||
### All CLI menu-pickers MUST use curses.
|
||||
Interactive menus must use `hermes_cli/curses_ui.py`. See `hermes_cli/tools_config.py` for an example.
|
||||
|
||||
### DO NOT use `\033[K` (ANSI erase-to-EOL) in spinner/display code
|
||||
Leaks as literal `?[K` text under `prompt_toolkit`'s `patch_stdout`. Use space-padding: `f"\r{line}{' ' * pad}"`.
|
||||
|
|
|
|||
|
|
@ -723,22 +723,9 @@ that touches the OS, assume *any* platform can hit your code path.
|
|||
For process enumeration: PowerShell's `Get-CimInstance Win32_Process` is
|
||||
the modern replacement for `wmic process`. See
|
||||
`hermes_cli/gateway.py::_scan_gateway_pids` for the pattern.
|
||||
|
||||
3. **`termios` and `fcntl` are Unix-only.** Always catch both `ImportError`
|
||||
and `NotImplementedError`:
|
||||
```python
|
||||
try:
|
||||
from simple_term_menu import TerminalMenu
|
||||
menu = TerminalMenu(options)
|
||||
idx = menu.show()
|
||||
except (ImportError, NotImplementedError):
|
||||
# Fallback: numbered menu for Windows
|
||||
for i, opt in enumerate(options):
|
||||
print(f" {i+1}. {opt}")
|
||||
idx = int(input("Choice: ")) - 1
|
||||
```
|
||||
|
||||
4. **File encoding.** Windows may save `.env` files in `cp1252`. Always
|
||||
3. **File encoding.** Windows may save `.env` files in `cp1252`. Always
|
||||
handle encoding errors:
|
||||
```python
|
||||
try:
|
||||
|
|
@ -750,7 +737,7 @@ that touches the OS, assume *any* platform can hit your code path.
|
|||
similar editors — use `encoding="utf-8-sig"` when reading files that
|
||||
could have been touched by a Windows GUI editor.
|
||||
|
||||
5. **Process management.** `os.setsid()`, `os.killpg()`, `os.fork()`,
|
||||
4. **Process management.** `os.setsid()`, `os.killpg()`, `os.fork()`,
|
||||
`os.getuid()`, and POSIX signal handling differ on Windows. Guard with
|
||||
`platform.system()`, `sys.platform`, or `hasattr(os, "setsid")`:
|
||||
```python
|
||||
|
|
@ -774,29 +761,29 @@ that touches the OS, assume *any* platform can hit your code path.
|
|||
pass
|
||||
```
|
||||
|
||||
6. **Signals that don't exist on Windows: `SIGALRM`, `SIGCHLD`, `SIGHUP`,
|
||||
5. **Signals that don't exist on Windows: `SIGALRM`, `SIGCHLD`, `SIGHUP`,
|
||||
`SIGUSR1`, `SIGUSR2`, `SIGPIPE`, `SIGQUIT`, `SIGKILL`.** Python's
|
||||
`signal` module raises `AttributeError` at import time if you reference
|
||||
them on Windows. Use `getattr(signal, "SIGKILL", signal.SIGTERM)` or
|
||||
gate the whole block behind a platform check. `loop.add_signal_handler`
|
||||
raises `NotImplementedError` on Windows — always catch it.
|
||||
|
||||
7. **Path separators.** Use `pathlib.Path` instead of string concatenation
|
||||
6. **Path separators.** Use `pathlib.Path` instead of string concatenation
|
||||
with `/`. Forward slashes work almost everywhere on Windows, but
|
||||
`subprocess.run(["cmd.exe", "/c", ...])` and other shell contexts can
|
||||
require backslashes — convert with `str(path)` at the subprocess boundary,
|
||||
not inside Python logic.
|
||||
|
||||
8. **Symlinks need elevated privileges on Windows** (unless Developer Mode is
|
||||
7. **Symlinks need elevated privileges on Windows** (unless Developer Mode is
|
||||
on). Tests that create symlinks need `@pytest.mark.skipif(sys.platform ==
|
||||
"win32", reason="Symlinks require elevated privileges on Windows")`.
|
||||
|
||||
9. **POSIX file modes (0o600, 0o644, etc.) are NOT enforced on NTFS** by
|
||||
8. **POSIX file modes (0o600, 0o644, etc.) are NOT enforced on NTFS** by
|
||||
default. Tests that assert on `stat().st_mode & 0o777` must skip on
|
||||
Windows — the concept doesn't translate. Use ACLs (`icacls`, `pywin32`)
|
||||
for Windows secret-file protection if needed.
|
||||
|
||||
10. **Detached background daemons on Windows need `pythonw.exe`, NOT
|
||||
9. **Detached background daemons on Windows need `pythonw.exe`, NOT
|
||||
`python.exe`.** `python.exe` always allocates or attaches to a console,
|
||||
which makes it vulnerable to `CTRL_C_EVENT` broadcasts from any sibling
|
||||
process. `pythonw.exe` is the no-console variant. Combine with
|
||||
|
|
@ -805,38 +792,38 @@ that touches the OS, assume *any* platform can hit your code path.
|
|||
See `hermes_cli/gateway_windows.py::_spawn_detached` for the reference
|
||||
implementation.
|
||||
|
||||
11. **`subprocess.Popen` with `.cmd` or `.bat` shims needs `shutil.which`
|
||||
10. **`subprocess.Popen` with `.cmd` or `.bat` shims needs `shutil.which`
|
||||
to resolve.** Passing `"agent-browser"` to `Popen` on Windows finds
|
||||
the extensionless POSIX shebang shim in `node_modules/.bin/`, which
|
||||
`CreateProcessW` can't execute — you'll get `WinError 193 "not a valid
|
||||
Win32 application"`. Use `shutil.which("agent-browser", path=local_bin)`
|
||||
which honors PATHEXT and picks the `.CMD` variant on Windows.
|
||||
|
||||
12. **Don't use shell shebangs as a way to run Python.** `#!/usr/bin/env
|
||||
11. **Don't use shell shebangs as a way to run Python.** `#!/usr/bin/env
|
||||
python` only works when the file is executed through a Unix shell.
|
||||
`subprocess.run(["./myscript.py"])` on Windows fails even if the file
|
||||
has a shebang line. Always invoke Python explicitly:
|
||||
`[sys.executable, "myscript.py"]`.
|
||||
|
||||
13. **Shell commands in installers.** If you change `scripts/install.sh`,
|
||||
12. **Shell commands in installers.** If you change `scripts/install.sh`,
|
||||
make the equivalent change in `scripts/install.ps1`. The two scripts
|
||||
are the canonical example of "works on Linux does not mean works on
|
||||
Windows" and have drifted multiple times — keep them in lockstep.
|
||||
|
||||
14. **Known paths that are OneDrive-redirected on Windows:** Desktop,
|
||||
13. **Known paths that are OneDrive-redirected on Windows:** Desktop,
|
||||
Documents, Pictures, Videos. The "real" path when OneDrive Backup is
|
||||
enabled is `%USERPROFILE%\OneDrive\Desktop` (etc.), NOT
|
||||
`%USERPROFILE%\Desktop` (which exists as an empty husk). Resolve the
|
||||
real location via `ctypes` + `SHGetKnownFolderPath` or by reading the
|
||||
`Shell Folders` registry key — never assume `~/Desktop`.
|
||||
|
||||
15. **CRLF vs LF in generated scripts.** Windows `cmd.exe` and `schtasks`
|
||||
14. **CRLF vs LF in generated scripts.** Windows `cmd.exe` and `schtasks`
|
||||
parse line-by-line; mixed or LF-only line endings can break multi-line
|
||||
`.cmd` / `.bat` files. Use `open(path, "w", encoding="utf-8",
|
||||
newline="\r\n")` — or `open(path, "wb")` + explicit bytes — when
|
||||
generating scripts Windows will execute.
|
||||
|
||||
16. **Two different quoting schemes in one command line.** `subprocess.run
|
||||
15. **Two different quoting schemes in one command line.** `subprocess.run
|
||||
(["schtasks", "/TR", some_cmd])` → schtasks itself parses `/TR`, AND
|
||||
the `some_cmd` string is re-parsed by `cmd.exe` when the task fires.
|
||||
Different parsers, different escape rules. Use two separate quoting
|
||||
|
|
@ -846,18 +833,15 @@ that touches the OS, assume *any* platform can hit your code path.
|
|||
|
||||
### Testing cross-platform
|
||||
|
||||
Tests that use POSIX-only syscalls need a skip marker. Common ones:
|
||||
- Symlinks → `@pytest.mark.skipif(sys.platform == "win32", ...)`
|
||||
- `0o600` file modes → `@pytest.mark.skipif(sys.platform.startswith("win"), ...)`
|
||||
- `signal.SIGALRM` → Unix-only (per-test timeouts no longer use it directly; see the win32 timeout-method shim in `tests/conftest.py::pytest_configure`)
|
||||
- `os.setsid` / `os.fork` → Unix-only
|
||||
- Live Winsock / Windows-specific regression tests →
|
||||
`@pytest.mark.skipif(sys.platform != "win32", reason="Windows-specific regression")`
|
||||
Tests that excercise behavior on specific platforms must run on their target platforms.
|
||||
|
||||
If you monkeypatch `sys.platform` for cross-platform tests, also patch
|
||||
`platform.system()` / `platform.release()` / `platform.mac_ver()` — each
|
||||
re-reads the real OS independently, so half-patched tests still route
|
||||
through the wrong branch on a Windows runner.
|
||||
```python
|
||||
@pytest.mark.linux_only
|
||||
@pytest.mark.macos_only
|
||||
@pytest.mark.windows_only
|
||||
```
|
||||
Avoid monkeypatching `sys.platform` unless absolutely needed, but if you do, also patch `platform.system()` / `platform.release()` / `platform.mac_ver()`.
|
||||
Symlinks, 0o600 permissions, SIGALRM, os.setsid/fork are all unix-only.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -7626,7 +7626,7 @@ def _prompt_model_selection(
|
|||
menu_title = "Select default model:"
|
||||
if has_pricing:
|
||||
# Align the header with the model column.
|
||||
# Each choice is " {label}" (2 spaces) and simple_term_menu prepends
|
||||
# Each choice is " {label}" (2 spaces) and we prepend
|
||||
# a 3-char cursor region ("-> " or " "), so content starts at col 5.
|
||||
pad = " " * 5
|
||||
header = f"\n{pad}{'':>{name_col}} {'In':>{price_col}} {'Out':>{price_col}}"
|
||||
|
|
@ -7639,10 +7639,6 @@ def _prompt_model_selection(
|
|||
menu_title += " ★ = on sale"
|
||||
|
||||
# Try arrow-key menu first, fall back to number input.
|
||||
# Uses the shared curses radiolist (ESC/arrow-key handling that works
|
||||
# across terminals, incl. those that emit raw escape sequences) instead
|
||||
# of simple_term_menu, which conflicts with /dev/tty and left ESC/arrow
|
||||
# keys unreliable in the setup model picker.
|
||||
try:
|
||||
from hermes_cli.curses_ui import curses_radiolist
|
||||
|
||||
|
|
|
|||
|
|
@ -340,11 +340,10 @@ def _handle_active_search_key(
|
|||
def flush_stdin() -> None:
|
||||
"""Flush any stray bytes from the stdin input buffer.
|
||||
|
||||
Must be called after ``curses.wrapper()`` (or any terminal-mode library
|
||||
like simple_term_menu) returns, **before** the next ``input()`` /
|
||||
``getpass.getpass()`` call. ``curses.endwin()`` restores the terminal
|
||||
but does NOT drain the OS input buffer — leftover escape-sequence bytes
|
||||
(from arrow keys, terminal mode-switch responses, or rapid keypresses)
|
||||
Must be called after ``curses.wrapper()`` returns, and before the next
|
||||
``input()`` / ``getpass.getpass()`` call.
|
||||
``curses.endwin()`` restores the terminal but does NOT drain the OS input buffer.
|
||||
Leftover escape-sequence bytes (from arrow keys, terminal mode-switch responses, or rapid keypresses)
|
||||
remain buffered and silently get consumed by the next ``input()`` call,
|
||||
corrupting user data (e.g. writing ``^[^[`` into .env files).
|
||||
|
||||
|
|
@ -874,8 +873,7 @@ def curses_single_select(
|
|||
) -> int | None:
|
||||
"""Curses single-select menu. Returns selected index or None on cancel.
|
||||
|
||||
Works inside prompt_toolkit because curses.wrapper() restores the terminal
|
||||
safely, unlike simple_term_menu which conflicts with /dev/tty.
|
||||
Works inside prompt_toolkit. curses.wrapper() restores the terminal safely.
|
||||
|
||||
When ``searchable`` is true, ``/`` opens a type-to-filter prompt; the
|
||||
returned value is always the original item index (or None for cancel).
|
||||
|
|
|
|||
|
|
@ -1082,8 +1082,6 @@ def _session_browse_picker(sessions: list) -> Optional[str]:
|
|||
"""Interactive curses-based session browser with live search filtering.
|
||||
|
||||
Returns the selected session ID, or None if cancelled.
|
||||
Uses curses (not simple_term_menu) to avoid the ghost-duplication rendering
|
||||
bug in tmux/iTerm when arrow keys are used.
|
||||
"""
|
||||
if not sessions:
|
||||
print("No sessions found.")
|
||||
|
|
|
|||
|
|
@ -192,7 +192,6 @@ matrix = ["mautrix[encryption]==0.21.0", "aiosqlite==0.22.1", "asyncpg==0.31.0",
|
|||
# and XXE. aiohttp/httpx are already in [messaging]; defusedxml lands
|
||||
# here to keep the dependency local to wecom_callback's threat model.
|
||||
wecom = ["defusedxml==0.7.1"]
|
||||
cli = ["simple-term-menu==1.6.6"]
|
||||
tts-premium = ["elevenlabs==1.59.0"]
|
||||
voice = [
|
||||
# Local STT pulls in wheel-only transitive deps (ctranslate2, onnxruntime).
|
||||
|
|
@ -280,7 +279,6 @@ termux = [
|
|||
# Baseline Android / Termux path for reliable fresh installs.
|
||||
"python-telegram-bot[webhooks]==22.6",
|
||||
"hermes-agent[cron]",
|
||||
"hermes-agent[cli]",
|
||||
"hermes-agent[mcp]",
|
||||
"hermes-agent[honcho]",
|
||||
"hermes-agent[acp]",
|
||||
|
|
@ -349,7 +347,6 @@ all = [
|
|||
# and failed on `make`. Lazy-install routes that build to first use,
|
||||
# where the user is expected to have a toolchain available.
|
||||
"hermes-agent[cron]",
|
||||
"hermes-agent[cli]",
|
||||
"hermes-agent[pty]",
|
||||
"hermes-agent[mcp]",
|
||||
"hermes-agent[homeassistant]",
|
||||
|
|
|
|||
|
|
@ -1,67 +0,0 @@
|
|||
"""Regression tests confirming the setup model/provider/reasoning pickers route
|
||||
through the shared curses radiolist (ESC + arrow-key handling that works across
|
||||
terminals, incl. Ghostty) instead of simple_term_menu.
|
||||
|
||||
Guards against silently regressing back to simple_term_menu, whose ESC/arrow
|
||||
handling was unreliable in `hermes setup` (the provider->model sub-menu).
|
||||
"""
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
def test_prompt_model_selection_uses_curses_radiolist():
|
||||
from hermes_cli.auth import _prompt_model_selection
|
||||
from hermes_cli.curses_ui import radio_item_plain
|
||||
|
||||
seen = {}
|
||||
|
||||
def _fake(
|
||||
title,
|
||||
items,
|
||||
*,
|
||||
selected=0,
|
||||
cancel_returns=None,
|
||||
description=None,
|
||||
searchable=False,
|
||||
search_labels=None,
|
||||
):
|
||||
seen["title"] = title
|
||||
seen["items"] = items
|
||||
seen["search_labels"] = search_labels
|
||||
return 1 # pick second model
|
||||
|
||||
with patch("hermes_cli.curses_ui.curses_radiolist", side_effect=_fake), \
|
||||
patch("builtins.print"):
|
||||
result = _prompt_model_selection(["model-a", "model-b"])
|
||||
|
||||
assert result == "model-b"
|
||||
assert seen["title"] == "Select default model:"
|
||||
# Items are the models plus the custom/skip entries. Model rows may be
|
||||
# rich (text, style) segments for sale chrome — compare the plain text.
|
||||
plain = [radio_item_plain(item) for item in seen["items"]]
|
||||
assert plain[:2] == ["model-a", "model-b"]
|
||||
assert "Skip (keep current)" in plain
|
||||
assert seen["search_labels"] is not None
|
||||
assert len(seen["search_labels"]) == len(seen["items"])
|
||||
|
||||
|
||||
def test_prompt_model_selection_esc_cancels():
|
||||
from hermes_cli.auth import _prompt_model_selection
|
||||
|
||||
# curses_radiolist returns the cancel sentinel (-1) on ESC.
|
||||
with patch("hermes_cli.curses_ui.curses_radiolist", return_value=-1), \
|
||||
patch("builtins.print"):
|
||||
result = _prompt_model_selection(["model-a", "model-b"])
|
||||
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_reasoning_effort_uses_curses_radiolist():
|
||||
from hermes_cli.main import _prompt_reasoning_effort_selection
|
||||
|
||||
with patch("hermes_cli.curses_ui.curses_radiolist", return_value=2), \
|
||||
patch("builtins.print"):
|
||||
result = _prompt_reasoning_effort_selection(["low", "medium", "high"], current_effort="")
|
||||
|
||||
assert result == "high"
|
||||
|
||||
|
||||
20
uv.lock
20
uv.lock
|
|
@ -1616,7 +1616,6 @@ all = [
|
|||
{ name = "mcp" },
|
||||
{ name = "pyasn1" },
|
||||
{ name = "python-multipart" },
|
||||
{ name = "simple-term-menu" },
|
||||
{ name = "starlette" },
|
||||
{ name = "uvicorn", extra = ["standard"] },
|
||||
{ name = "youtube-transcript-api" },
|
||||
|
|
@ -1630,9 +1629,6 @@ azure-identity = [
|
|||
bedrock = [
|
||||
{ name = "boto3" },
|
||||
]
|
||||
cli = [
|
||||
{ name = "simple-term-menu" },
|
||||
]
|
||||
computer-use = [
|
||||
{ name = "mcp" },
|
||||
{ name = "starlette" },
|
||||
|
|
@ -1744,7 +1740,6 @@ termux = [
|
|||
{ name = "honcho-ai" },
|
||||
{ name = "mcp" },
|
||||
{ name = "python-telegram-bot", extra = ["webhooks"] },
|
||||
{ name = "simple-term-menu" },
|
||||
{ name = "starlette" },
|
||||
]
|
||||
termux-all = [
|
||||
|
|
@ -1761,7 +1756,6 @@ termux-all = [
|
|||
{ name = "pyasn1" },
|
||||
{ name = "python-multipart" },
|
||||
{ name = "python-telegram-bot", extra = ["webhooks"] },
|
||||
{ name = "simple-term-menu" },
|
||||
{ name = "starlette" },
|
||||
{ name = "uvicorn", extra = ["standard"] },
|
||||
]
|
||||
|
|
@ -1845,8 +1839,6 @@ requires-dist = [
|
|||
{ name = "google-auth-oauthlib", marker = "extra == 'google'", specifier = "==1.3.1" },
|
||||
{ name = "hermes-agent", extras = ["acp"], marker = "extra == 'all'" },
|
||||
{ name = "hermes-agent", extras = ["acp"], marker = "extra == 'termux'" },
|
||||
{ name = "hermes-agent", extras = ["cli"], marker = "extra == 'all'" },
|
||||
{ name = "hermes-agent", extras = ["cli"], marker = "extra == 'termux'" },
|
||||
{ name = "hermes-agent", extras = ["cron"], marker = "extra == 'all'" },
|
||||
{ name = "hermes-agent", extras = ["cron"], marker = "extra == 'termux'" },
|
||||
{ name = "hermes-agent", extras = ["google"], marker = "extra == 'all'" },
|
||||
|
|
@ -1918,7 +1910,6 @@ requires-dist = [
|
|||
{ name = "sentencepiece", marker = "extra == 'wake'", specifier = "==0.2.2" },
|
||||
{ name = "setuptools", marker = "extra == 'dev'", specifier = "==83.0.0" },
|
||||
{ name = "sherpa-onnx", marker = "extra == 'wake'", specifier = "==1.13.4" },
|
||||
{ name = "simple-term-menu", marker = "extra == 'cli'", specifier = "==1.6.6" },
|
||||
{ name = "slack-bolt", marker = "extra == 'messaging'", specifier = "==1.29.0" },
|
||||
{ name = "slack-bolt", marker = "extra == 'slack'", specifier = "==1.29.0" },
|
||||
{ name = "slack-sdk", marker = "extra == 'messaging'", specifier = "==3.43.0" },
|
||||
|
|
@ -1940,7 +1931,7 @@ requires-dist = [
|
|||
{ name = "websockets", specifier = "==15.0.1" },
|
||||
{ name = "youtube-transcript-api", marker = "extra == 'youtube'", specifier = "==1.2.4" },
|
||||
]
|
||||
provides-extras = ["anthropic", "exa", "firecrawl", "parallel-web", "fal", "edge-tts", "modal", "daytona", "vercel", "hindsight", "dev", "messaging", "cron", "slack", "matrix", "wecom", "cli", "tts-premium", "voice", "wake", "honcho", "supermemory", "mem0", "vision", "pty", "mcp", "nemo-relay", "homeassistant", "sms", "teams", "computer-use", "acp", "mistral", "otlp", "bedrock", "vertex", "azure-identity", "termux", "termux-all", "dingtalk", "feishu", "google", "youtube", "web", "all"]
|
||||
provides-extras = ["anthropic", "exa", "firecrawl", "parallel-web", "fal", "edge-tts", "modal", "daytona", "vercel", "hindsight", "dev", "messaging", "cron", "slack", "matrix", "wecom", "tts-premium", "voice", "wake", "honcho", "supermemory", "mem0", "vision", "pty", "mcp", "nemo-relay", "homeassistant", "sms", "teams", "computer-use", "acp", "mistral", "otlp", "bedrock", "vertex", "azure-identity", "termux", "termux-all", "dingtalk", "feishu", "google", "youtube", "web", "all"]
|
||||
|
||||
[[package]]
|
||||
name = "hf-xet"
|
||||
|
|
@ -4169,15 +4160,6 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/82/40/ee8a0a8c83fc6d7f5245a5a031e471d3b115e20cce867e7abb2f9d4185c9/sherpa_onnx-1.13.4-cp313-cp313-win_amd64.whl", hash = "sha256:17050fdfb48d37ae996364f697c554a1399740d18e5a56b143c011d00cfed3e0", size = 2244504, upload-time = "2026-07-07T12:32:59.882Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simple-term-menu"
|
||||
version = "1.6.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/80/f0f10b4045628645a841d3d98b584a8699005ee03a211fc7c45f6c6f0e99/simple_term_menu-1.6.6.tar.gz", hash = "sha256:9813d36f5749d62d200a5599b1ec88469c71378312adc084c00c00bfbb383893", size = 35493, upload-time = "2024-12-02T16:31:50.639Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/09/21d993e394c1fe5c44cd90453d88ed44932da8dfca006e424c072d77d29b/simple_term_menu-1.6.6-py3-none-any.whl", hash = "sha256:c2a869efa7a9f7e4a9c25858b42ca6974034951c137d5e281f5339b06ed8c9c2", size = 27600, upload-time = "2024-12-02T16:31:48.934Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "six"
|
||||
version = "1.17.0"
|
||||
|
|
|
|||
|
|
@ -159,23 +159,7 @@ When contributing code, keep these rules in mind:
|
|||
|
||||
Key patterns:
|
||||
|
||||
### 1. `termios` and `fcntl` are Unix-only
|
||||
|
||||
Always catch both `ImportError` and `NotImplementedError`:
|
||||
|
||||
```python
|
||||
try:
|
||||
from simple_term_menu import TerminalMenu
|
||||
menu = TerminalMenu(options)
|
||||
idx = menu.show()
|
||||
except (ImportError, NotImplementedError):
|
||||
# Fallback: numbered menu
|
||||
for i, opt in enumerate(options):
|
||||
print(f" {i+1}. {opt}")
|
||||
idx = int(input("Choice: ")) - 1
|
||||
```
|
||||
|
||||
### 2. File encoding
|
||||
### 1. File encoding
|
||||
|
||||
Some environments may save `.env` files in non-UTF-8 encodings:
|
||||
|
||||
|
|
@ -186,7 +170,7 @@ except UnicodeDecodeError:
|
|||
load_dotenv(env_path, encoding="latin-1")
|
||||
```
|
||||
|
||||
### 3. Process management
|
||||
### 2. Process management
|
||||
|
||||
`os.setsid()`, `os.killpg()`, and signal handling differ across platforms:
|
||||
|
||||
|
|
@ -196,7 +180,7 @@ if platform.system() != "Windows":
|
|||
kwargs["preexec_fn"] = os.setsid
|
||||
```
|
||||
|
||||
### 4. Path separators
|
||||
### 3. Path separators
|
||||
|
||||
Use `pathlib.Path` instead of string concatenation with `/`.
|
||||
|
||||
|
|
|
|||
|
|
@ -132,24 +132,7 @@ Hermes 官方支持 **Linux、macOS、WSL2 以及原生 Windows(通过 PowerSh
|
|||
- **使用 `pathlib.Path` / `os.path.join`,不得手动用 `/` 拼接路径。** 这对我们构造后传给子进程的字符串尤为重要,而非 OS 返回给我们的字符串。
|
||||
|
||||
关键模式:
|
||||
|
||||
### 1. `termios` 和 `fcntl` 仅适用于 Unix
|
||||
|
||||
始终同时捕获 `ImportError` 和 `NotImplementedError`:
|
||||
|
||||
```python
|
||||
try:
|
||||
from simple_term_menu import TerminalMenu
|
||||
menu = TerminalMenu(options)
|
||||
idx = menu.show()
|
||||
except (ImportError, NotImplementedError):
|
||||
# 回退:编号菜单
|
||||
for i, opt in enumerate(options):
|
||||
print(f" {i+1}. {opt}")
|
||||
idx = int(input("Choice: ")) - 1
|
||||
```
|
||||
|
||||
### 2. 文件编码
|
||||
### 1. 文件编码
|
||||
|
||||
某些环境可能以非 UTF-8 编码保存 `.env` 文件:
|
||||
|
||||
|
|
@ -160,7 +143,7 @@ except UnicodeDecodeError:
|
|||
load_dotenv(env_path, encoding="latin-1")
|
||||
```
|
||||
|
||||
### 3. 进程管理
|
||||
### 2. 进程管理
|
||||
|
||||
`os.setsid()`、`os.killpg()` 以及信号处理在各平台间存在差异:
|
||||
|
||||
|
|
@ -170,7 +153,7 @@ if platform.system() != "Windows":
|
|||
kwargs["preexec_fn"] = os.setsid
|
||||
```
|
||||
|
||||
### 4. 路径分隔符
|
||||
### 3. 路径分隔符
|
||||
|
||||
使用 `pathlib.Path` 代替用 `/` 进行字符串拼接。
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue