refactor(hermes_cli): pets — drop getattr defensive layers on argparse namespaces, compact docstrings
This commit is contained in:
+32
-61
@@ -16,7 +16,7 @@ def _cmd_list(args) -> int:
|
||||
"""List gallery pets (or only installed ones with ``--installed``)."""
|
||||
from agent.pet import store
|
||||
|
||||
if getattr(args, "installed", False):
|
||||
if args.installed:
|
||||
pets = store.installed_pets()
|
||||
if not pets:
|
||||
print("No pets installed. Try: hermes pets install boba")
|
||||
@@ -34,11 +34,11 @@ def _cmd_list(args) -> int:
|
||||
_err(f"✗ {exc}")
|
||||
return 1
|
||||
|
||||
query = (getattr(args, "query", "") or "").strip().lower()
|
||||
query = (args.query or "").strip().lower()
|
||||
if query:
|
||||
entries = [e for e in entries if query in e.slug.lower() or query in e.display_name.lower()]
|
||||
|
||||
limit = getattr(args, "limit", 0) or 0
|
||||
limit = args.limit or 0
|
||||
shown = entries[:limit] if limit > 0 else entries
|
||||
installed = {p.slug for p in store.installed_pets()}
|
||||
|
||||
@@ -58,14 +58,14 @@ def _cmd_install(args) -> int:
|
||||
|
||||
slug = args.slug.strip()
|
||||
try:
|
||||
pet = store.install_pet(slug, force=getattr(args, "force", False))
|
||||
pet = store.install_pet(slug, force=args.force)
|
||||
except (store.PetStoreError, ManifestError) as exc:
|
||||
_err(f"✗ install failed: {exc}")
|
||||
return 1
|
||||
|
||||
print(f"✓ installed {pet.display_name} → {pet.directory}")
|
||||
|
||||
if getattr(args, "select", False) or not _has_active_pet():
|
||||
if args.select or not _has_active_pet():
|
||||
_set_active(slug)
|
||||
print(f"✓ {pet.display_name} is now the active pet (display.pet.slug={slug}, enabled)")
|
||||
else:
|
||||
@@ -87,7 +87,7 @@ def _cmd_remove(args) -> int:
|
||||
def _cmd_select(args) -> int:
|
||||
from agent.pet import store
|
||||
|
||||
slug = (getattr(args, "slug", "") or "").strip()
|
||||
slug = (args.slug or "").strip()
|
||||
if not slug:
|
||||
pets = store.installed_pets()
|
||||
if not pets:
|
||||
@@ -124,12 +124,9 @@ def _cmd_scale(args) -> int:
|
||||
|
||||
|
||||
def _cmd_show(args) -> int:
|
||||
"""Animate the active (or named) pet in the terminal.
|
||||
|
||||
Uses the shared :class:`~agent.pet.render.PetRenderer` — full graphics protocol
|
||||
(kitty/iTerm2/sixel) when the terminal supports it, else a truecolor Unicode half-block
|
||||
fallback. Ctrl+C to stop.
|
||||
"""
|
||||
"""Animate the active (or named) pet in the terminal via the shared PetRenderer
|
||||
(kitty/iTerm2/sixel when supported, else truecolor half-block fallback). Ctrl+C stops."""
|
||||
import shutil
|
||||
import time
|
||||
|
||||
from agent.pet import store
|
||||
@@ -137,31 +134,26 @@ def _cmd_show(args) -> int:
|
||||
from agent.pet.render import build_renderer
|
||||
|
||||
cfg = _pet_config()
|
||||
slug = (getattr(args, "slug", "") or "").strip() or str(cfg.get("slug", "") or "")
|
||||
slug = (args.slug or "").strip() or str(cfg.get("slug", "") or "")
|
||||
pet = store.resolve_active_pet(slug)
|
||||
if pet is None:
|
||||
_err("✗ no pet to show — run: hermes pets install boba")
|
||||
return 1
|
||||
|
||||
mode_cfg = getattr(args, "mode", None) or str(cfg.get("render_mode", "auto") or "auto")
|
||||
scale = float(getattr(args, "scale", 0) or cfg.get("scale", DEFAULT_SCALE) or DEFAULT_SCALE)
|
||||
mode_cfg = args.mode or str(cfg.get("render_mode", "auto") or "auto")
|
||||
scale = float(args.scale or cfg.get("scale", DEFAULT_SCALE) or DEFAULT_SCALE)
|
||||
cols = resolve_cols(scale, cfg.get("unicode_cols", 0))
|
||||
|
||||
renderer = build_renderer(
|
||||
pet.spritesheet,
|
||||
configured_mode=mode_cfg,
|
||||
scale=scale,
|
||||
unicode_cols=cols,
|
||||
)
|
||||
renderer = build_renderer(pet.spritesheet, configured_mode=mode_cfg, scale=scale, unicode_cols=cols)
|
||||
if not renderer.available:
|
||||
_err(f"✗ cannot render here (no TTY / graphics disabled). Effective mode: {renderer.mode}.")
|
||||
return 1
|
||||
|
||||
# Which states to play: one named state, or cycle the driveable rows.
|
||||
requested = (getattr(args, "state", "") or "").strip().lower()
|
||||
requested = (args.state or "").strip().lower()
|
||||
if requested:
|
||||
states = [requested]
|
||||
elif getattr(args, "cycle", False):
|
||||
elif args.cycle:
|
||||
states = [s for s in STATE_ROWS if s in {e.value for e in PetState}]
|
||||
else:
|
||||
states = [PetState.IDLE.value]
|
||||
@@ -169,11 +161,8 @@ def _cmd_show(args) -> int:
|
||||
is_unicode = renderer.mode == "unicode"
|
||||
frame_delay = max(0.05, (LOOP_MS / 1000.0) / max(1, renderer.frame_count(states[0]) or 1))
|
||||
|
||||
# Right-align the sprite against the terminal's right edge — half-blocks by
|
||||
# indenting each row, graphics protocols by padding the cursor to the right
|
||||
# column before the image draws (kitty/iTerm/sixel all render at the cursor).
|
||||
import shutil
|
||||
|
||||
# Right-align against the terminal's right edge — half-blocks by indenting each row, graphics
|
||||
# protocols by padding the cursor (kitty/iTerm/sixel all render at the cursor).
|
||||
term_cols = shutil.get_terminal_size((80, 24)).columns
|
||||
sprite_cols = cols if is_unicode else max(1, int(renderer.frame_w * renderer.scale) // 8)
|
||||
indent = " " * max(0, term_cols - sprite_cols - 1)
|
||||
@@ -187,8 +176,7 @@ def _cmd_show(args) -> int:
|
||||
loops = 0
|
||||
while True:
|
||||
for state in states:
|
||||
count = renderer.frame_count(state) or 1
|
||||
for i in range(count):
|
||||
for i in range(renderer.frame_count(state) or 1):
|
||||
encoded = renderer.frame(state, i)
|
||||
if is_unicode:
|
||||
if indent:
|
||||
@@ -197,8 +185,7 @@ def _cmd_show(args) -> int:
|
||||
out.write(f"\x1b[{prev_lines}F") # cursor up to redraw in place
|
||||
out.write(encoded)
|
||||
out.write("\x1b[0m\n")
|
||||
# Lines drawn = sprite rows + the trailing newline; move
|
||||
# back up exactly that many so the next frame overwrites.
|
||||
# Lines drawn = sprite rows + trailing newline; the next frame overwrites.
|
||||
prev_lines = encoded.count("\n") + 1
|
||||
else:
|
||||
out.write("\x1b[2J\x1b[3J\x1b[H") # clear for image protocols
|
||||
@@ -207,7 +194,7 @@ def _cmd_show(args) -> int:
|
||||
out.flush()
|
||||
time.sleep(frame_delay)
|
||||
loops += 1
|
||||
if getattr(args, "once", False) and loops >= len(states):
|
||||
if args.once and loops >= len(states):
|
||||
break
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
@@ -261,9 +248,7 @@ def _cmd_doctor(args) -> int:
|
||||
return 0
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# config helpers
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# ── config helpers ────────────────────────────────────────────────────────
|
||||
|
||||
def _pet_config() -> dict:
|
||||
from hermes_cli.config import load_config
|
||||
@@ -284,11 +269,10 @@ def _pet_enabled(cfg: dict) -> bool:
|
||||
|
||||
|
||||
def _update_pet_config(when_slug: str | None = None, **values) -> bool:
|
||||
"""Write ``display.pet.*`` keys and save config.
|
||||
"""Write ``display.pet.*`` keys and save config; returns whether anything was written.
|
||||
|
||||
With ``when_slug`` the write only happens when the currently configured slug equals it (used
|
||||
by remove/rename so an inactive pet never disturbs the active one). Returns whether anything
|
||||
was written.
|
||||
With ``when_slug`` the write only happens when the configured slug equals it (remove/rename
|
||||
must never disturb a different active pet).
|
||||
"""
|
||||
from hermes_cli.config import load_config, save_config
|
||||
|
||||
@@ -312,12 +296,9 @@ def _set_enabled(enabled: bool) -> None:
|
||||
|
||||
|
||||
def set_pet_scale(value: float | str) -> tuple[float, str | None]:
|
||||
"""Set ``display.pet.scale`` (clamped to bounds). Returns ``(applied, error)``.
|
||||
|
||||
The single write path behind ``/pet scale`` and the desktop slider, so every surface that
|
||||
resolves scale from config picks it up identically. *error* is set (and nothing written) only
|
||||
when *value* isn't a number.
|
||||
"""
|
||||
"""Set ``display.pet.scale`` (clamped). Returns ``(applied, error)``; *error* is set (and
|
||||
nothing written) only when *value* isn't a number. Single write path behind ``/pet scale``
|
||||
and the desktop slider."""
|
||||
from agent.pet.constants import clamp_scale
|
||||
|
||||
try:
|
||||
@@ -334,8 +315,7 @@ def toggle_pet_display() -> tuple[bool, str | None, str | None]:
|
||||
from agent.pet import store
|
||||
|
||||
cfg = _pet_config()
|
||||
slug = str(cfg.get("slug", "") or "")
|
||||
pet = store.resolve_active_pet(slug)
|
||||
pet = store.resolve_active_pet(str(cfg.get("slug", "") or ""))
|
||||
|
||||
if _pet_enabled(cfg):
|
||||
_set_enabled(False)
|
||||
@@ -373,20 +353,13 @@ def print_pet_gallery(*, limit: int = 20) -> None:
|
||||
|
||||
|
||||
def _clear_active_if(slug: str) -> bool:
|
||||
"""Disable + unset the active pet iff it's ``slug`` (e.g. after removal).
|
||||
|
||||
Returns whether anything changed, so callers don't write config needlessly.
|
||||
"""
|
||||
"""Disable + unset the active pet iff it's ``slug`` (e.g. after removal)."""
|
||||
return _update_pet_config(when_slug=slug, slug="", enabled=False)
|
||||
|
||||
|
||||
def _rename_active_if(old_slug: str, new_slug: str) -> bool:
|
||||
"""Repoint the active pet from ``old_slug`` to ``new_slug`` iff it's active.
|
||||
|
||||
Used when a rename realigns a pet's slug/dir: if the renamed pet was the active one, the config
|
||||
must follow or surfaces point at a now-missing dir. Preserves the ``enabled`` flag. Returns
|
||||
whether anything changed.
|
||||
"""
|
||||
"""Repoint the active pet from ``old_slug`` to ``new_slug`` iff it's active (a rename moves
|
||||
the dir; config must follow). Preserves ``enabled``."""
|
||||
if not new_slug or old_slug == new_slug:
|
||||
return False
|
||||
return _update_pet_config(when_slug=old_slug, slug=new_slug)
|
||||
@@ -408,9 +381,7 @@ def _interactive_pick(pets) -> str:
|
||||
return ""
|
||||
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# argparse wiring
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
# ── argparse wiring ───────────────────────────────────────────────────────
|
||||
|
||||
# (name, help, handler, [((flags...), add_argument kwargs), ...]) — registration order is menu order.
|
||||
_SUBCOMMANDS = (
|
||||
|
||||
Reference in New Issue
Block a user