Metadata-Version: 2.3
Name: sioba
Version: 0.5.2050911
Summary: Sioba IO Abstraction Layer
License: MIT License
         
         Copyright (c) 2025 Aki Mimoto
         
         Permission is hereby granted, free of charge, to any person obtaining a copy
         of this software and associated documentation files (the "Software"), to deal
         in the Software without restriction, including without limitation the rights
         to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
         copies of the Software, and to permit persons to whom the Software is
         furnished to do so, subject to the following conditions:
         
         The above copyright notice and this permission notice shall be included in all
         copies or substantial portions of the Software.
         
         THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
         IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
         FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
         AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
         LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
         OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
         SOFTWARE.
Classifier: License :: OSI Approved :: MIT License
Requires-Dist: loguru>=0.7.2
Requires-Dist: rich>=14.0.0
Requires-Dist: pyte>=0.8.2
Requires-Dist: loguru>=0.7.2
Requires-Dist: janus>=2.0.0
Requires-Python: >=3.10
Project-URL: Homepage, https://github.com/amimoto/sioba
Project-URL: Issues, https://github.com/amimoto/sioba/issues
Description-Content-Type: text/markdown

# sioba — IO abstraction layer for terminal-like interfaces

A small Python library that unifies interactive IO (functions, TCP/SSL sockets) behind a single async `Interface` API with pluggable scrollback buffers (terminal emulator or simple line buffer).

<img src="https://raw.githubusercontent.com/amimoto/sioba/refs/heads/main/sioba/sioba-image.png" width="300" />

---

## Overview

* **Interfaces**: concrete implementations for `echo://`, `tcp://host:port`, and `ssl://host:port`, plus a `FunctionInterface` to wrap regular Python functions into an interactive session.
* **Buffers (scrollback/state)**: choose `terminal://` (ANSI/VT via **pyte**) or `line://` (raw lines). Both keep cursor position and bounded history.
* **URI-driven config**: `InterfaceContext.from_uri()` parses connection + runtime options (rows/cols/title/encoding/convertEol/auto\_shutdown, etc.).
* **Callbacks**: hook `on_send_to_frontend`, `on_receive_from_frontend`, `on_shutdown`, `on_set_terminal_title`.
* **Plugin registries**: register new interface schemes or buffers via decorators, or expose them via entry points.
* **Rich integration**: `Interface.filehandle()` returns a TTY-like handle (isatty=True) so `rich.Console` writes render with color.

---

## Installation

**From source (recommended until published):**

```bash
pip install sioba
```

**Using uv:**

```bash
uv pip install sioba
```

* Requires **Python ≥ 3.10**.
* Runtime deps: `loguru`, `rich`, `pyte`, `janus`.

---

## Quickstart

### Echo interface (minimal)

```python
import asyncio
from sioba import interface_from_uri, Interface

async def main():
    # uri-like creation of interface instances for consistent
    # invocation syntax that can be easily put into a conf
    echo = await interface_from_uri("echo://").start()

    # Example of callback
    captured = []
    async def on_send_to_frontend(_i: Interface, data: bytes):
        captured.append(data)
    echo.on_send_to_frontend(on_send_to_frontend)

    # Manually injec tdata into the interface instance
    await echo.receive_from_frontend(b"Hello, World!")

    # We should have received the data in our callback
    print(captured[0])           # b"Hello, World!"

    # Finally cleanup
    await echo.shutdown()

asyncio.run(main())
```

---

## Core Concepts / Features

* **Interface & lifecycle**

  * `sioba.interface_from_uri("tcp://host:1234?rows=52&cols=100")` parses URI + query into fields (scheme, host, port, rows, cols, etc.) creates InterfaceContext, Buffer, and the Interface itself.
  * `Interface` manages state (`INITIALIZED` → `STARTED` → `SHUTDOWN`), send/receive, callbacks, and a **buffer**.
  * `await interface.start()` then interact via `receive_from_frontend()` (input) and `on_send_to_frontend()` (output).
  * `get_terminal_buffer()` returns a snapshot of current screen/buffer; `get_terminal_cursor_position()` exposes (row, col).

* **Context**

  * `InterfaceContext.from_uri("tcp://host:1234?rows=52&cols=100")` parses URI + query into fields (scheme, host, port, rows, cols, etc.).
  * Defaults: rows=24, cols=80, encoding=`utf-8`, `convertEol=True`, `auto_shutdown=True`, `scrollback_buffer_uri="terminal://"`, `scrollback_buffer_size=10000`.

* **Buffers**

  * `terminal://` uses **pyte** to emulate ANSI terminals (title changes, scrollback, style attributes, cursor updates).
  * `line://` stores raw lines with simple newline splitting.

* **Registries & plugins**

  * Interfaces: decorate with `@register_scheme("myscheme")` or provide entry points under `sioba.interface`.
  * Buffers: `@register_buffer("mybuffer")` or entry point `sioba.buffer`.

---

## High Level Architecture

sioba is composed of 3 primary concepts: **transport/control** from **screen state** and **configuration**:

* **Interface** (`sioba.interface.base.Interface`) owns the lifecycle, transports, and callback wiring.
* **Buffer** (`sioba.buffer.base.Buffer` and implementations) is a sidecar that maintains a *screen/scrollback view* of what the user should see.
* **InterfaceContext** (`sioba.context.InterfaceContext`) is the shared config/runtime state that both Interface and Buffer read and update.

---

### 1 Interface (transport + lifecycle)

**Where it lives:** `sioba.interface.base.Interface` (base class) with concrete types like:

* `EchoInterface` (`echo://`) (`sioba.interface.echo`)
* `SocketInterface` (`tcp://host:port?args`) & `SecureSocketInterface` (`ssl://host:port?args`) (`sioba.interface.socket`)
* `FunctionInterface` (`sioba.interface.function`): Base class, doesn't provide direct functionality but makes scripted behaviors possible

**What it does:**

* Manages **state** (`InterfaceState.INITIALIZED/STARTED/SHUTDOWN`) and lifecycle: `await start()`, `await shutdown()`.
* Normalizes **IO**:

  * `receive_from_frontend(data)` handles *incoming* user data (e.g., keystrokes). If `convertEol=True`, `\r\n`/`\r` → `\n`. Calls an overridable `receive_from_frontend_handle`.
  * `send_to_frontend(data)` handles *outgoing* data (e.g., server output). If `convertEol=True`, `\n` → `\r\n`. Calls an overridable `send_to_frontend_handle`.
  * **Always** feeds outgoing bytes to the **Buffer** (`await buffer.feed(data)`) before dispatching to registered `on_send_to_frontend` callbacks.
* Wires **callbacks**: `on_send_to_frontend`, `on_receive_from_frontend`, `on_shutdown`, `on_set_terminal_title`.
* Owns a **Buffer** instance, created from the context’s `scrollback_buffer_uri` via `buffer_from_uri(...)`.
* Exposes **screen helpers** backed by the Buffer:

  * `get_terminal_buffer()` → bytes snapshot (after optional EOL conversion).
  * `get_terminal_cursor_position()` → `(row, col)`.
  * `set_terminal_size(...)` → delegates to the Buffer.
  * `update_terminal_metadata({...})` → merges per-client sizes and applies the *smallest* rows/cols.
* Provides a **file-like** handle (`filehandle()`) via `VirtualIO` so libraries like `rich` can write colored output to the interface.

**Transport examples:**

* `SocketInterface` opens an asyncio TCP stream, reads in a background task, and echoes user input locally while writing to the socket.
* `SecureSocketInterface` does the same over TLS; its scheme is registered with a **custom context class** (`SecureSocketConfig`) to accept a `create_ssl_context` callable.
* `FunctionInterface` runs your function in a thread; it offers `print()`, `input()`, `getpass()` built on internal queues and capture modes.

**Discovery / plugins:**

* New schemes register with `@register_scheme("myscheme", context_class=...)`.
* Or ship via **entry points** (`sioba.interface`)—loaded by `interface_from_uri("myscheme://...")`.

---

### 2 Buffer (screen/scrollback model)

**Where it lives:** `sioba.buffer.base.Buffer` (base) with two built-ins:

* `LineBuffer` (`sioba.buffer.line`, URI: `line://`) – minimal, newline-split lines.
* `TerminalBuffer` (`sioba.buffer.terminal`, URI: `terminal://`) – ANSI/VT handling via **pyte**.

**What it does:**

* Receives every **outgoing** byte stream from the Interface (`feed(data)`) and updates a persistent view of the screen.
* Maintains **scrollback** bounded by `context.scrollback_buffer_size` (+rows for the line buffer).
* Provides `dump_screen_state()` to serialize the view (used by `Interface.get_terminal_buffer()`).

* `TerminalBuffer` is based upon pyte to provide a terminal code aware buffer.
  * It overrides `set_title` to call `interface.set_terminal_title(...)`, which updates `context.title` and fires title callbacks.
  * Tracks **cursor position** and **title** and writes these into the shared **Context**:
  * Responds to window **resize**: `set_terminal_size(rows, cols, ...)`.

**Discovery / plugins:**

* New buffers register with `@register_buffer("mybuffer")` or via entry points under `sioba.buffer`.
* Selected by the context’s `scrollback_buffer_uri` and created through `buffer_from_uri("terminal://", interface=..., ...)`.

---

### 3 InterfaceContext (shared config + runtime state)

**Where it lives:** `sioba.context.InterfaceContext` (a dataclass).

**How it’s created:**

* From a URI: `InterfaceContext.from_uri("tcp://host:1234?rows=52&cols=100")`
* Or filled with defaults: `InterfaceContext.with_defaults(...)`

**What it holds:**

* **Connection/identity**: `uri, scheme, host, port, username, password, query, extra_params`.
* **Terminal geometry & behavior**: `rows, cols, encoding, convertEol, auto_shutdown`.
* **Buffer config**: `scrollback_buffer_uri` (default `"terminal://"`), `scrollback_buffer_size` (default `10000`).
* **Runtime metadata** written by buffers: `cursor_row`, `cursor_col`, `title`.

**Type-aware URI parsing:**

* Query params are cast to the right types (int/bool/etc.) via `cast_str_to_type`.
* `update()`, `copy()`, and `asdict()` help compose and inspect context instances.

**Per-scheme context:**

* An Interface can declare a specialized `context_class` (e.g., `SecureSocketConfig` for SSL) when registering the scheme; `interface_from_uri` uses it to build the context from the URI.

---

## Data flow (two directions)

### 1) Frontend → Interface (user input)

```
bytes from UI
   └─► Interface.receive_from_frontend(...)
        ├─ if convertEol: normalize CR/LF to '\n'
        ├─ call receive_from_frontend_handle(...)  # transport-specific
        └─ fire on_receive_from_frontend callbacks
```

* Example behaviors:

  * `SocketInterface`: writes to the socket (and locally echoes).
  * `FunctionInterface`: parses input by line / control chars (Enter, Ctrl-C, Backspace) and pushes through its queues.

### 2) Transport/app → Interface → Buffer → UI (output)

```
producer (socket read, function print)
   └─► Interface.send_to_frontend(data)
        ├─ if convertEol: '\n' → '\r\n'
        ├─ send_to_frontend_handle(...)   # optional, per interface
        ├─ Buffer.feed(data)              # updates scrollback, cursor, title
        └─ fire on_send_to_frontend callbacks (UI emit)
```

* Because **Buffer.feed** sits in the send path, the UI’s **screen snapshot** and **cursor/title** are always consistent with what was emitted.

---

## Lifecycle & sizing

* `await interface.start()` transitions to **STARTED** and lets the concrete interface initialize its tasks/threads (e.g., open sockets, start read loop, spawn function thread).
* `await interface.shutdown()` runs `shutdown_handle()`, switches to **SHUTDOWN**, then notifies `on_shutdown` callbacks.
* UI can call `update_terminal_metadata({"rows": R, "cols": C}, client_id=...)`. The Interface chooses the **smallest** rows/cols across all clients (tmux-like behavior) and updates the Buffer via `set_terminal_size(...)`.

---

## Extending: new Interfaces & Buffers

* **New transport**: subclass `Interface`, implement `start_interface`, `receive_from_frontend_handle`, optional `send_to_frontend_handle`/`shutdown_handle`, then `@register_scheme("yours")` (optionally with a custom `context_class`).
* **New buffer**: subclass `Buffer`, implement `initialize`, `feed`, `dump_screen_state`, and optional `set_terminal_size`, then `@register_buffer("yours")`.
* **Plugin packaging**: expose classes through entry points `sioba.interface` or `sioba.buffer` so `interface_from_uri(...)` / `buffer_from_uri(...)` can discover them dynamically.

Absolutely—here’s a focused “how-to extend sioba” that shows subclassing, decorators, entry points, and how `InterfaceContext.from_uri()` is used under the hood to create instances.

---

### How discovery works (registry + entry points)

* At import time, using the decorators:

  * `@register_scheme("name", context_class=...)` adds an `Interface` subclass to the in-process registry.
  * `@register_buffer("name")` adds a `Buffer` subclass to the buffer registry.

* Lazy discovery via **entry points** (recommended for plugins):

  * If a scheme/buffer isn’t already in the registry, `interface_from_uri` / `buffer_from_uri` will look up the relevant entry point group and `ep.load()` the class:

    * Interfaces: group **`sioba.interface`**
    * Buffers: group **`sioba.buffer`**

This means your plugin can be installed as a separate package and only loaded on demand when a user calls `interface_from_uri("yourscheme://...")` or selects your buffer via `scrollback_buffer_uri="yourbuffer://"`.

---

### The creation pipeline (what happens under the hood)

When you call:

```python
iface = interface_from_uri("myscheme://host:1234?rows=40&cols=100", my_flag=True)
```

1. `interface_from_uri` parses the URI (`urlparse`), extracts the **scheme** (`myscheme`).
2. If not already registered, it searches entry points in group `sioba.interface`, loads the matching class, verifies it subclasses `Interface`, and caches it in the registry.
3. It picks a **context class**:

   * If your handler set `context_class` in `@register_scheme(..., context_class=MyContext)`, that’s used.
   * Otherwise the default `InterfaceContext` is used.
4. It builds the **context** with `context_class.from_uri(uri, **kwargs)`:

   * `from_uri` parses `host/port/query` and **casts** known fields to the right types.
   * Any extra kwargs you pass (e.g., `my_flag=True`) are merged if they match fields on the context dataclass.
   * Defaults are filled via `with_defaults(...)` (rows=24, cols=80, `convertEol=True`, `auto_shutdown=True`, `scrollback_buffer_uri="terminal://"`, etc.).
5. It constructs your `Interface` with that context and any provided callbacks.
6. Inside `Interface.__init__`, the chosen **Buffer** is created via `buffer_from_uri(context.scrollback_buffer_uri, interface=self, ...)`. If the buffer scheme isn’t registered, the loader resolves it via entry points in group `sioba.buffer`.
7. You then `await iface.start()`; your subclass’s `start_interface()` is called to actually connect/launch resources.

---

## Example 1: A custom Interface with a custom Context

### 1a) Define a context (extend `InterfaceContext`)

```python
# mypkg/myproto/context.py
from dataclasses import dataclass
from sioba import InterfaceContext

@dataclass
class MyProtoContext(InterfaceContext):
    # Parsed from URI query or kwargs in interface_from_uri(...)
    my_flag: bool = False
    timeout_ms: int = 5000
```

Notes:

* `InterfaceContext.from_uri()` will **cast** `?my_flag=true&timeout_ms=2500` to the right types.
* You get all the base fields too (`host`, `port`, `rows`, `cols`, `encoding`, `scrollback_buffer_uri`, etc.).

### 1b) Implement the Interface

```python
# mypkg/myproto/interface.py
import asyncio
from sioba.interface.base import Interface, InterfaceState, register_scheme
from .context import MyProtoContext

@register_scheme("myproto", context_class=MyProtoContext)
class MyProtoInterface(Interface):
    reader: asyncio.StreamReader | None = None
    writer: asyncio.StreamWriter | None = None
    _recv_task: asyncio.Task | None = None

    async def start_interface(self) -> bool:
        # Mark started (base does this as well; harmless to be explicit).
        self.state = InterfaceState.STARTED

        ctx: MyProtoContext = self.context  # typed context
        # Use URI parts parsed into context (e.g., host/port) and custom fields:
        self.reader, self.writer = await asyncio.open_connection(ctx.host, ctx.port)
        self._recv_task = asyncio.create_task(self._receive_loop(timeout_ms=ctx.timeout_ms))
        return True

    async def _receive_loop(self, timeout_ms: int):
        try:
            while self.state == InterfaceState.STARTED:
                data = await asyncio.wait_for(self.reader.read(4096), timeout_ms/1000)
                if not data:
                    break
                await self.send_to_frontend(data)   # feeds Buffer, fires callbacks
        except asyncio.TimeoutError:
            # Optional: emit a heartbeat or warning
            pass
        finally:
            await self.shutdown()

    async def receive_from_frontend_handle(self, data: bytes) -> None:
        # normalize CR/LF already handled; you may add protocol framing here:
        if self.writer:
            self.writer.write(data)
            await self.writer.drain()
            # Local echo if you want (SocketInterface does this):
            await self.send_to_frontend(data)

    async def shutdown_handle(self) -> None:
        if self._recv_task:
            self._recv_task.cancel()
        if self.writer:
            self.writer.close()
            try:
                await self.writer.wait_closed()
            except ConnectionAbortedError:
                pass
```

### 1c) Use it

```python
from sioba import interface_from_uri

iface = await interface_from_uri(
    "myproto://example.com:9000?rows=40&cols=100&my_flag=true&timeout_ms=2000"
).start()

# interact
await iface.receive_from_frontend(b"HELLO\n")
buf = iface.get_terminal_buffer()
await iface.shutdown()
```

Everything above mirrors the built-in `SocketInterface`/`SecureSocketInterface` pattern, but with your own knobs on the context.

---

## Example 2: A custom Buffer

A buffer transforms **outgoing** bytes into a durable “screen” snapshot and updates cursor/title on the shared Context.

```python
# mypkg/mybuffer/buffer.py
from sioba.buffer.base import Buffer, register_buffer

@register_buffer("ring")
class RingBuffer(Buffer):
    """
    A tiny byte ring buffer (no ANSI processing).
    """
    def initialize(self, **extra):
        self.capacity = int(extra.get("capacity", 8192))
        self._buf = bytearray()

    async def feed(self, data: bytes) -> None:
        # Called for every Interface.send_to_frontend(data)
        self._buf.extend(data)
        if len(self._buf) > self.capacity:
            # drop oldest
            drop = len(self._buf) - self.capacity
            del self._buf[:drop]

        # Maintain a “cursor” approximation on the shared context
        # (optional but consistent with built-ins):
        text = bytes(self._buf).split(b"\n")[-1]
        col = len(text)
        # rows/cols are bounds in Context; keep within them:
        rows = self.interface.context.rows
        cols = self.interface.context.cols
        row = min(rows - 1, len(bytes(self._buf).splitlines()))
        col = min(cols - 1, col)
        self.interface.context.cursor_row = row
        self.interface.context.cursor_col = col

    def dump_screen_state(self) -> bytes:
        return bytes(self._buf)

    def set_terminal_size(self, rows: int, cols: int, xpix: int = 0, ypix: int = 0) -> None:
        # Nothing needed for a raw ring buffer; could trim to new “visible” size if desired.
        pass
```

Use it by pointing a context at `ring://`:

```python
from sioba import Interface, InterfaceContext

ctx = InterfaceContext.with_defaults(
    title="RB",
    rows=10, cols=40,
    scrollback_buffer_uri="ring://",    # uses our RingBuffer
)
iface = Interface(context=ctx)
await iface.start()
await iface.send_to_frontend(b"hello\nworld\n")
print(iface.get_terminal_buffer())     # bytes from our ring
await iface.shutdown()
```

> Internals: `Interface.__init__` calls `buffer_from_uri(context.scrollback_buffer_uri, interface=self, on_set_terminal_title=self.set_terminal_title)`. If your buffer scheme isn’t in the process registry, it’s resolved via the `sioba.buffer` entry point group.

---

## Example 3: Packaging as a plugin with entry points

To make your `myproto` interface and `ring` buffer discoverable **without importing your module explicitly**, expose them via entry points in your package’s `pyproject.toml`:

```toml
[project]
name = "mypkg-sioba-plugins"
version = "0.1.0"
requires-python = ">=3.10"
dependencies = ["sioba>=0.3"]  # pin to your desired version

[project.entry-points]
"sioba.interface".myproto = "mypkg.myproto.interface:MyProtoInterface"
"sioba.buffer".ring = "mypkg.mybuffer.buffer:RingBuffer"
```

Now, anywhere `sioba` is installed:

```python
from sioba import interface_from_uri

# "myproto" is lazily discovered via entry points:
iface = await interface_from_uri("myproto://example.com:9000?rows=40&cols=100").start()
```

No need to import `mypkg.myproto.interface` manually; `interface_from_uri` will load it on demand.

---

## Tips & conventions (from sioba internals)

* **Context casting**: add fields to your custom context (dataclass) and pass values via URI query (`?flag=true`) or kwargs to `interface_from_uri(...)`. `from_uri` will **cast** strings to the declared types (int/bool/float).
* **EOL policy**: `Interface.receive_from_frontend` normalizes CR/LF to `\n`; `send_to_frontend` converts `\n` to `\r\n` if `convertEol=True`. Your transport code should assume normalized `\n` on input and rely on `send_to_frontend` for outbound normalization.
* **Screen & title**:

  * Buffers should call/trigger `interface.set_terminal_title(title)` when titles change (the terminal buffer does this inside `EventsScreen.set_title`).
  * Buffers should update `context.cursor_row/col` so `Interface.get_terminal_cursor_position()` remains accurate.
* **Lifecycle**: override `start_interface()` and `shutdown_handle()` for resources; always handle cancellation and closed streams cleanly.
* **Async vs thread**: if you need sync code, see `FunctionInterface` for a queue-driven pattern and capture modes (`ECHO`, `DISCARD`, `INPUT`, `GETPASS`).

---

### End-to-end checklist for a new scheme

1. Create a `@dataclass class MyContext(InterfaceContext)` with any extra fields you need.
2. Implement `@register_scheme("myscheme", context_class=MyContext)` on your subclass of `Interface`.
3. (Optional) Implement a custom buffer and `@register_buffer("mybuffer")`.
4. Package both via `pyproject.toml` entry points:

   * `"sioba.interface".myscheme = "pkg.mod:Class"`
   * `"sioba.buffer".mybuffer = "pkg.mod:Class"`
5. Use it:

   ```python
   await interface_from_uri("myscheme://host:1234?rows=40&my_flag=true").start()
   ```

That’s it—your interface/buffer will be discoverable and type-safe, created with a fully-populated `InterfaceContext` derived from the URI plus any kwargs you pass.


---

## A few concrete behaviors tied together

* **EOL policy** is centralized:

  * Incoming keystrokes normalized (`\r\n` / `\r` → `\n`) before reaching a transport.
  * Outgoing data normalized (`\n` → `\r\n`) before it hits the Buffer/UI.
* **Title changes** originate in the **Buffer** (e.g., `TerminalBuffer`’s `EventsScreen.set_title`) and propagate up through `Interface.set_terminal_title(...)` to the **Context** and any registered callbacks.
* **Cursor position** is a **Context** field (`cursor_row`, `cursor_col`) maintained by the Buffer (via `EventsCursor` in the terminal buffer, or computed in the line buffer).
* **Rich integration** uses `Interface.filehandle()` (a `TextIOBase` that sets `isatty=True`) so things like `rich.Console(file=...)` print straight into the Interface; internally that just calls `send_to_frontend(...)`, which flows through the Buffer and callbacks.

---

That’s the architectural core: **Interface** moves bytes and orchestrates the session, **Buffer** turns bytes into a durable screen model (plus cursor/title), and **Context** binds configuration and runtime metadata that both of the others read and write.


---

## Usage

### 1) `FunctionInterface` (interactive script)

```python
import asyncio, time
from sioba import FunctionInterface, Interface

def app(ui: FunctionInterface):
    ui.print("Welcome!")
    name = ui.input("What's your name? ")
    ui.print(f"Hello, {name}!")
    hidden = ui.getpass("Enter your hidden word: ")
    ui.print(f"Length noted: {len(hidden)}")
    time.sleep(0.2)

async def main():
    f = FunctionInterface(app)
    await f.start()
    # Simulate terminal input for the two prompts:
    await f.receive_from_frontend(b"Mochi\r\n")
    await f.receive_from_frontend(b"Wasabi\r\n")
    # Read what the function printed to the screen:
    print(f.get_terminal_buffer().decode("utf-8", errors="replace"))
    await f.shutdown()

asyncio.run(main())
```

* Input capture modes (internal): `ECHO`, `DISCARD`, `INPUT`, `GETPASS`. The prompts above demonstrate `INPUT` and `GETPASS`.

### 2) TCP / SSL sockets

```python
import asyncio, ssl
from sioba import interface_from_uri, SocketInterface, SecureSocketInterface

async def tcp_demo():
    sock = await interface_from_uri("tcp://localhost:12345").start()
    out = []
    sock.on_send_to_frontend(lambda _i, d: out.append(d))
    await sock.receive_from_frontend(b"HELLO\n")
    await asyncio.sleep(0.1)
    await sock.shutdown()

async def ssl_demo():
    ctx = ssl._create_unverified_context()  # example from tests
    ssli = await interface_from_uri(
        "ssl://localhost:12345",
        create_ssl_context=lambda _cfg: ctx,  # SecureSocketConfig hook
    ).start()
    await ssli.shutdown()
```

### 3) Choosing a buffer & reading state

```python
from sioba import Interface, InterfaceContext

ctx = InterfaceContext.with_defaults(
    title="Demo",
    scrollback_buffer_uri="terminal://",   # or "line://"
    rows=5, cols=80,
)
iface = Interface(context=ctx)
await iface.start()
await iface.send_to_frontend(b"Rich text and ANSI go here\n")
print(iface.get_terminal_buffer())         # bytes snapshot
await iface.shutdown()
```

### 4) Registering a custom scheme / buffer

```python
from sioba import register_scheme, register_buffer
from sioba.interface.base import Interface

@register_scheme("myproto")
class MyProto(Interface):
    async def receive_from_frontend_handle(self, data: bytes):
        await self.send_to_frontend(b"ok:" + data)

@register_buffer("dummy")
class DummyBuffer:
    pass
```

---

## API Highlights

* `sioba.Interface` — base class: lifecycle, callbacks, buffer, screen helpers.
* `sioba.InterfaceContext` — dataclass; parse/update/copy context; `from_uri`, `with_defaults`, `asdict`, `get`.
* `sioba.interface_from_uri(uri, **kw)` — build interface from `scheme://…` (+ optional context overrides).
* `sioba.register_scheme(*schemes, context_class=None)` / `sioba.list_schemes()` — plugin registration & discovery.
* `sioba.FunctionInterface` — wrap a Python function with `print()`, `input()`, `getpass()` over an interface.
* `sioba.EchoInterface` — `echo://` passthrough for testing.
* `sioba.SocketInterface` — `tcp://host:port` using asyncio streams.
* `sioba.SecureSocketInterface` — `ssl://host:port` with optional `create_ssl_context`.
* `sioba.buffer_from_uri(uri, **kw)` / `sioba.register_buffer(*names)` / `sioba.list_buffer_schemes()` — buffer plugins.
* `sioba.errors` — `InterfaceNotStarted`, `InterfaceShutdown`, `TerminalClosedError`, etc.
* `sioba.Interface.filehandle()` — TTY-like stream (used by `rich.Console(file=...)`).

---

## CLI

> TODO: No CLI entry points found; add one if a command-line tool is intended.

---

## Security & Limits

* `tcp://` and `ssl://` interfaces **open network connections**; user input is echoed locally and forwarded to the remote server.
* `SecureSocketInterface` accepts a custom SSL context; using an “unverified” context (as in tests) **disables certificate verification**—unsafe for production.
* `FunctionInterface` runs your function in a **separate thread** and can execute arbitrary code; there is no sandboxing.

---

## Compatibility & Requirements

* Python **≥ 3.10**.
* Tested interfaces/buffers use `asyncio`, `pyte`, `rich`, `loguru`, `janus`.
* Defaults: `convertEol=True` (outgoing `\n` → `\r\n`), encoding `utf-8`.

---

## Contributing

```bash
# clone
git clone https://github.com/amimoto/sioba
cd sioba

# (dev) install with uv
uv sync

# run tests
uv run pytest -q
# or, if not using uv:
pytest -q
```

---

## License

sioba is available under the MIT license. See the LICENSE file for more info.

---
