Add logemit event callbacks and kanta.tty terminal formatting

All change-related output (transactions, bootstrap, migrations) is now
described by a mutable LogEvent carrying full state plus the preferred
logger and level, and dispatched through emit_event. @kanta.logemit
callbacks receive the event and decide what is logged where: falsy return
marks it handled, truthy passes it (possibly modified) down the chain,
with default_emit - Kanta's own formatting, now just another emitter - as
the fallback. Pretty header and diff lines are lazy event properties.

New kanta.tty module: Line builder (call to append content, .colorname
arms a palette color for the next call with automatic folded reset,
width/align padding), a mutable Colors palette storing bare SGR params
(0 clears, sequential last-wins stacking), and strip_ansi/displaywidth/pad
helpers that count wide chars and emoji correctly.
This commit is contained in:
Leo Vasanko
2026-08-07 05:11:54 +00:00
parent e42f81f44f
commit 6eb9087863
12 changed files with 758 additions and 138 deletions
+48
View File
@@ -222,6 +222,54 @@ def resolve_user_key(value: str) -> str | None:
`kanta.transaction.diff` child logger so applications can route or silence
them separately from the headers.
#### Log Emitters
- Every change-related message Kanta emits (transaction/bootstrap/migration
changes, `Created <file>`, migration summaries) is described by a
`kanta.logging.LogEvent` and dispatched through `kanta.logging.emit_event`.
Kanta's own output goes through the same mechanism: when no `logemit`
callback handles an event, `kanta.logging.default_emit` renders it with the
built-in formatting.
- A `LogEvent` carries the event `kind` (`"change"`, `"created"`,
`"migrated"`), the preferred `logger` and `level`, and all relevant state:
`action`, `user`, `extra`, `diff`, `previous`/`current` state dicts, the
built `logfmt` chain, and version info for migration events. Pretty
`header` and `diff_lines` are lazy properties, built only if accessed.
- `@kanta.logemit` registers a callback receiving the event. The callback
decides what is logged and where: it may log one or more messages on
`event.logger`, log somewhere else, or nothing at all. A falsy return
value marks the event handled and stops the chain; a truthy return value
passes the event — possibly modified — to the next registered callback.
When all callbacks pass, `default_emit` renders the event; a callback may
also call `default_emit(event)` itself to delegate events it does not
customize. Operational diagnostics (rollback warnings, integrity errors)
do not go through this mechanism.
```python
@kanta.logemit
def emit(ev: LogEvent):
if ev.kind != "change":
return default_emit(ev) # delegate, no chaining needed
actor = ev.current.get("users", {}).get(ev.user, {}).get("name", ev.user)
line = Line().user(actor, width=20)(" ").action(ev.action)
ev.logger.log(ev.level, f"{line}\n" + "\n".join(ev.diff_lines))
```
#### Terminal Formatting Helpers
- `kanta.tty` provides the building blocks used by Kanta's own rendering:
- `colors`: the mutable color palette. Colors are bare SGR parameter
strings (e.g. `"1;34"`, `"38;5;226"`) without escape framing. Attributes
are read at render time, so assignments (`colors.action = "36"`) and
additions (`colors.session = "38;5;226"`) take effect immediately.
- `Line`: builds a terminal string part by part. Calling it appends
content (`str`-converted); `.<colorname>` arms a palette color for the
next call only, and the reset is folded into a single escape sequence
with whatever color comes next. `width=`/`align=` pad by display width;
`str(line)` finishes the line and restores default colors.
- `strip_ansi`, `displaywidth` (wide chars and emoji count correctly) and
`pad` for working with pre-colored strings.
## Migrations
- Migration source is configured on `Kanta(...)` via `migrations=`.