# pyos-tui

A terminal-first mini-OS framework for building curses apps in Python. Provides an activity stack, event loop, flex layout engine, and composable UI components — so you can focus on your app instead of wrestling with curses.

## Install

```bash
pip install pyos-tui

# Windows users
pip install pyos-tui[windows]
```

## Quick start

```python
import curses
from pyos import Application, Activity
from pyos.EventTypes import KeyStroke
from pyos.Keys import ESC
from pyos.printers.TopBar import TopBar
from pyos.printers.ScrollList import ScrollList
from pyos.printers.BottomBar import BottomBar
from pyos.input_handlers import handle_scroll_list_input, ScrollChange

class HomeActivity(Activity):
    def on_start(self):
        self.application.subscribe(KeyStroke, self, self.on_key)
        self.application.subscribe(ScrollChange, self, self.on_scroll)

        self.display_state = {
            "top": TopBar.display_state(items={"title": "My App", "help": "ESC quit"}),
            "list": ScrollList.display_state(
                screen=self.screen,
                items=[f"Item {i}" for i in range(1, 51)],
                selected_index=0,
                focused=True,
                input_handler=handle_scroll_list_input,
                min_height=5, flex=1,
            ),
            "bottom": BottomBar.display_state(items={"status": "Ready"}),
        }
        self.refresh_screen()

    def on_key(self, event):
        self.display_state["list"]["input_handler"](
            "list", self.display_state["list"], event, self.event_queue
        )
        if event.key == ESC:
            self.application.pop_activity()
        self.refresh_screen()

    def on_scroll(self, event):
        idx = self.display_state["list"]["selected_index"]
        self.display_state["bottom"]["items"]["status"] = f"Selected: {idx}"
        self.refresh_screen()

def main(stdscr):
    app = Application(stdscr)
    app.start(HomeActivity())

if __name__ == "__main__":
    curses.wrapper(main)
```

## Features

**Activity stack** — Push, pop, and replace screens like a mobile navigation controller. Each Activity owns its UI state and subscriptions; the framework cleans up automatically on pop.

**Event system** — Subscribe to typed events (`KeyStroke`, `TextBoxSubmit`, `ScrollChange`, etc.). Activities subscribe in `on_start` and the framework unsubscribes everything on stop.

**Flex layout** — Regions can be fixed-height, flex (proportional with min/max), or auto-measured. The layout solver distributes terminal rows automatically.

**Composable UI components** — Built-in printers for common patterns:

| Component | Description |
|---|---|
| `TopBar` / `BottomBar` | Status bars |
| `ScrollList` | Scrollable list with selection |
| `TextInput` | Single-line text field with cursor |
| `Table` | Auto-sized columns with headers |
| `ContextMenuList` | List items with inline action menus |
| `MultilineText` | Read-only text block |
| `Accordion` | Collapsible sections |
| `Spacer` | Fills remaining space |

**Input handlers** — Plug-in handlers for text fields (`handle_text_box_input`) and scroll lists (`handle_scroll_list_input`) that manage cursor, selection, and emit events.

**Threading** — `CentralDispatch` provides serial and concurrent dispatch queues. The main queue is safe for UI mutations; background work marshals updates back via `main_thread.submit_async(...)`.

**Error recovery** — Unhandled exceptions push a traceback viewer. Press ESC to attempt stack recovery without crashing.

**Built-in log viewer** — Press F1 to tail `application.log` in a dedicated activity.

**Pytest plugin** — Ships a headful test renderer (`pyos-headful`) as a pytest plugin for watching your TUI tests render in real time.

## Activity lifecycle

```
_start(application)  →  on_start()  →  refresh_screen()  →  _stop()
```

Override `on_start()` to set up `display_state` and event subscriptions. Override `on_stop()` for cleanup. Navigate with:

```python
self.application.segue_to(NextActivity())              # push
self.application.segue_to(NextActivity(), Segue.REPLACE)  # replace
self.application.pop_activity()                        # pop (empty stack stops the app)
```

## Layout

Each entry in `display_state` is a dict with a `"layout"` key:

```python
{"height": 3}                                    # fixed: exactly 3 rows
{"flex": 1, "min_height": 5}                     # flex: share remaining space
{"flex": 2, "min_height": 3, "max_height": 20}   # flex with bounds
# omit both → auto-measured from line_generator output
```

## Threading

Only read/write `display_state` on the main thread. From background work:

```python
self.main_thread.submit_async(self.refresh_screen)
self.main_thread.submit_async(self.on_new_data, payload)
```

## Requirements

- Python 3.9+
- A terminal that supports curses (most Unix terminals; Windows via `windows-curses`)

## License

MIT
