voice editing: 'space [<n>]' inserts spaces, 'backspace [<n>]' (alias delete) deletes chars, 'erase' (alias clear/wipe) wipes the current input. the daemon tracks a per-session uncommitted-input char count so backspace is capped at the last submit boundary and erase clears exactly back to it; submit/set reset it. keys.py gains BSpace/space; grammar gains a count parser (digits + number words). new console.py renders every daemon line as 'HH:MM:SS [prefix] message' with color: [<session>] for injected lines (green), [SYSTEM] for state, [VOICE] for recognition/drops (red/dim). bump to 0.1.2. Signed-off-by: disqualifier <dev@disqualifier.me>
52 lines
1.8 KiB
Python
52 lines
1.8 KiB
Python
"""colored, prefixed console output for the daemon's recognition/action feed.
|
|
|
|
every line is ``HH:MM:SS [PREFIX] message``. prefixes group the source: a session
|
|
name (e.g. ``[claude-libs]``) for anything injected into a tmux session, ``[SYSTEM]``
|
|
for daemon-control/state lines, and ``[VOICE]`` for STT/recognition lines. color is
|
|
opt-in via tty detection (or forced): green for successful injections, red for
|
|
drops/errors, dim for routine. falls back to plain text when stdout is not a tty.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
import time
|
|
|
|
RESET = "\033[0m"
|
|
_COLORS = {
|
|
"green": "\033[32m",
|
|
"red": "\033[31m",
|
|
"yellow": "\033[33m",
|
|
"cyan": "\033[36m",
|
|
"dim": "\033[2m",
|
|
"bold": "\033[1m",
|
|
}
|
|
|
|
SYSTEM = "SYSTEM"
|
|
VOICE = "VOICE"
|
|
|
|
|
|
class Console:
|
|
"""formats and prints daemon log lines with timestamp, prefix, and color"""
|
|
|
|
def __init__(self, color: bool | None = None, stream=None, clock=None) -> None:
|
|
self.stream = stream if stream is not None else sys.stdout
|
|
self._clock = clock or time.localtime
|
|
if color is None:
|
|
color = hasattr(self.stream, "isatty") and self.stream.isatty()
|
|
self.color = bool(color)
|
|
|
|
def _stamp(self) -> str:
|
|
t = self._clock()
|
|
return f"{t.tm_hour:02d}:{t.tm_min:02d}:{t.tm_sec:02d}"
|
|
|
|
def _paint(self, text: str, color: str | None) -> str:
|
|
if not self.color or not color or color not in _COLORS:
|
|
return text
|
|
return f"{_COLORS[color]}{text}{RESET}"
|
|
|
|
def emit(self, prefix: str, message: str, color: str | None = None) -> None:
|
|
"""print one line: ``HH:MM:SS [prefix] message`` (message optionally colored)"""
|
|
line = f"{self._stamp()} {self._paint(f'[{prefix}]', 'dim')} {self._paint(message, color)}"
|
|
print(line, file=self.stream, flush=True)
|