Metadata-Version: 2.4
Name: eosh
Version: 0.3.0
Summary: A lightweight but powerful terminal shell with rich TAB completion and context switching
Author: craftware
License-Expression: MIT
Project-URL: Homepage, https://github.com/crftwr/eosh
Project-URL: Repository, https://github.com/crftwr/eosh
Project-URL: Issues, https://github.com/crftwr/eosh/issues
Keywords: shell,terminal,completion,repl,aws
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: System Administrators
Classifier: Operating System :: MacOS
Classifier: Operating System :: POSIX
Classifier: Operating System :: Microsoft :: Windows
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: System :: Shells
Classifier: Topic :: Terminals
Requires-Python: >=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: awsut
Requires-Dist: boto3; extra == "awsut"
Requires-Dist: pexpect; extra == "awsut"
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: eosh[awsut]; extra == "dev"
Dynamic: license-file

<!-- Generated from README.md by scripts/gen_pypi_readme.py; relative
     links rewritten against the v0.3.0 tag for PyPI. Do not edit
     or commit. -->

# Eolith Shell

**Eolith Shell** (`eosh`) is a lightweight but powerful terminal shell environment with rich tab completion and context switching.


![Eolith Shell demo: TAB completion with descriptions, flag completion, and switching contexts while a build keeps running](https://raw.githubusercontent.com/crftwr/eosh/v0.3.0/doc/images/demo.gif)

## Features

- **Rich tab completion** — per-argument completers with descriptions, inline picker UI
- **Flag completion** — TAB on `-` lists every flag with its description; a flag that takes a value goes straight on to completing that value
- **Context switching** — named environments with variables and working directories, switched by name or live with `Ctrl+]`
- **PTY process multiplexing** — run processes in contexts and switch between them without killing them
- **Pipelines and redirections** — `|`, `>`, `>>`, `<`, `2>`, `2>&1`, `;`, `&&`, `||`, globs (`*`, `?`, `**`), and `\`-line-continuation
- **Pipeline decorators** — wrap any pipeline with `@watch`, `@time`, `@retry` or `@quiet`; authoring your own is a few lines of Python
- **Custom commands** — define Python functions as shell commands with full completion support
- **Python-backed variables** — `var aws_region=us-east-1` can drive multiple `os.environ` keys via a `Var` subclass; `$NAME` expansion is symmetric
- **Completion recipes** — opt-in TAB completion for `git`, `make`, `ssh`, `aws`, and more
- **Protocol fallbacks** — completion for cobra-based tools (`docker`, `kubectl`, `helm`, `gh`, …, opt-in by name) and, automatically, argcomplete-based Python CLIs (`pipx`, `conda`, `pre-commit`, `tox`, …) — no hand-written recipe needed
- **System command fallback** — anything not a registered command runs through the system shell
- **Cross-platform** — interactive shell, completion, pipelines, redirects, contexts, and history all work on POSIX and Windows; PTY-backed multiplexing of running native processes is POSIX-only
- **History** — shared by every eosh window (SQLite): fish-style suggestions of lines you ran in this directory, `Ctrl+R` search that starts from what you've typed, per-context up/down navigation, and a `history` command
- **Desktop notifications** — an OS notification when a command that took more than 10 seconds finishes, including ones running in a backgrounded context; native backends only, no extra dependencies

## Installation

Requires Python 3.12+. The core has no dependencies beyond the standard library.

```bash
pip install eosh               # or: pipx install eosh / uv tool install eosh
pip install "eosh[awsut]"      # + the `awsut` add-on (boto3, pexpect)
uv tool install "eosh[awsut]"  # same, as an isolated uv tool
```

On first launch `eosh` writes a starter `~/.eosh/config.py` that enables
every built-in recipe and bundled add-on. Without the `[awsut]` extra,
`awsut` is skipped and everything else loads normally; running `awsut` then
says which module is missing and which extra provides it.

`uv tool` and `pipx` install eosh into its own virtualenv, so a
`pip install boto3` in another shell does not reach it — add packages with
`uv tool install eosh --with <pkg>` or `pipx inject eosh <pkg>`. With
`uv tool`, `--with` *replaces* the previous list, so repeat any earlier
entries (`uv tool list --show-with` shows them).

### From source

```bash
git clone https://github.com/crftwr/eosh
cd eosh
make install      # .venv/ with `pip install -e ".[dev]"`
make test
make run
```

## Usage

```bash
eosh
```

### Built-in Commands

| Command | Description |
|---------|-------------|
| `cd [path]` | Change directory (default: home) |
| `help [command]` | Show help for a command or list all commands |
| `context` | Manage contexts (see below) |
| `var [NAME[=VALUE] ...]` | List variables, print one (`var NAME`), set (`var NAME=VALUE`), or unset (`var NAME=`) |
| `alias [NAME[=EXPANSION] ...]` | List aliases, show one, or define a shorthand for a command |
| `unalias NAME [NAME ...]` | Remove aliases |
| `source-bash [FILE [ARG ...]]` | Run a bash script (or a pasted block) and import its variables and cwd |
| `confirm [MESSAGE]` | Ask `MESSAGE [y/N]` (default `Continue?`); exit 0 on `y`, 1 otherwise — `confirm 'Destroy prod?' && terraform destroy` |
| `history [-n N] [--here] [KEYWORD ...]` | List past command lines (every window, every context), filtered by keywords or this directory |
| `reload` | Reload `~/.eosh/config.py` without restarting |
| `config edit` | Open `~/.eosh/config.py` in `$VISUAL` / `$EDITOR`, then reload it |
| `exit` | Exit the shell |

Any command not listed above is passed through to the system shell (e.g., `ls`, `git`, `grep`).

### Aliases

An alias replaces a command's name with text, as in bash: at the start of a line, after `;` `&&` `||` `|`, and as a decorator's body. The text can be anything a line can hold — pipes, `;` / `&&`, redirects, `$VAR`, decorators. Quote the expansion when it contains spaces:

```
eosh> alias hp='awsut sagemaker hyperpod'
eosh> hp list                  # runs: awsut sagemaker hyperpod list
eosh> alias gl='git log --oneline | head -20'
eosh> alias deploy='confirm "Deploy to prod?" && make deploy'
eosh> alias                    # list all aliases
eosh> unalias hp
```

`\hp` or `'hp'` runs the command itself, not the alias. An alias may use another (`alias ll='ls -l'` with `alias ls='ls -G'` runs `ls -G -l`), but never itself, so `alias ls='ls -G'` is safe.

### Importing Variables from Bash

`source-bash` runs a script in a real `bash` — so `export`, `$(…)`, loops, heredocs and conditionals all work — and imports the environment and working directory it leaves behind, the way bash's own `source` would:

```
eosh> source-bash              # paste `export KEY=VALUE` lines, end with a blank line or Ctrl+D
eosh> source-bash setup.sh s3  # source a file with arguments
eosh> source-bash -c 'export A=$(date +%s)'
```

`--no-cd` keeps the current directory and `-q` suppresses the summary of imported variable names. Shell functions, aliases and shell options can't be imported.

### Contexts

Contexts let you define named environments with variables that are exported to `os.environ` and a remembered working directory.

```
eosh> context new prod
Created context 'prod'
[prod] eosh> var ACCOUNT=123456 REGION=us-east-1
[prod] eosh> context new staging
Created context 'staging'
[staging] eosh> var ACCOUNT=789012 REGION=us-west-2
[staging] eosh> context list
  * staging {'ACCOUNT': '789012', 'REGION': 'us-west-2'}
    prod {'ACCOUNT': '123456', 'REGION': 'us-east-1'}
    default
[staging] eosh> context close
Closed 'staging', now in 'prod'
```

Context subcommands:

| Subcommand | Description |
|------------|-------------|
| `context new <name>` | Create a context (inheriting the current one's variables and history) and switch to it |
| `context close [name]` | Remove a context (default: the current one); the most recently used remaining one becomes current |
| `context switch <name>` | Switch to an existing context |
| `context list` | Show all contexts with their state and variables |
| `context kill <name>` | Send SIGTERM to the running process in a context |

### Context Switching with Ctrl+]

Press `Ctrl+]` at the shell prompt (or while a process is running) to open a TUI context picker:

- Arrow keys or `Ctrl+P` to navigate
- **Enter** to switch to the selected context
- **Esc** / `Ctrl+C` to cancel
- `Ctrl+N` creates a new context, `Ctrl+D` deletes the highlighted one, `Ctrl+R` renames it

If the target context has a running process, switching to it resumes that process immediately. The original process keeps running in the background — visible as `[bg:1]` in the prompt.

### Tab Completion

Press TAB to complete:
- Command names (registered commands + system PATH executables)
- File/directory paths (default fallback)
- Custom per-argument completions defined by commands

### History

As you type, the most recent line you ran **in this directory** that starts with what you've typed appears dimmed after the cursor. **→** (or Ctrl+E / End) accepts it, and **Alt+F** accepts one word. Otherwise keep typing:

```
~/proj> git commit -m "fix typo"
              ^^^^^^^^^^^^^^^^^^ typed "git co"; the rest is the suggestion
```

- **Ctrl+R** searches every line you've run, anywhere. It starts filtered by what's already typed, and shows where and when each line last ran.
- **`history`** lists past lines: `history docker run`, `history -n 100 --here`.
- **↑ / ↓** walk the current context's own list.

History is shared by every eosh window you have open (`~/.eosh/history.db`, SQLite). A line run in one window is suggested in another at once, and shells running side by side never overwrite each other's history. See [doc/history.md](https://github.com/crftwr/eosh/blob/v0.3.0/doc/history.md).

**Flag completion** — TAB on `-` lists the command's flags as ordinary picker
rows, each with its description:
- Type to narrow, Down/Up and **Enter** to pick — one flag per TAB
- A flag that takes a value is shown as `-d <N>`; picking it inserts `-d ` and
  goes straight on to completing the value (a directory picker for `tar -C`,
  say). When the value has no completer, the status bar shows what to type.

### Pipelines, Redirects, and Sequencing

eosh supports the operators you'd expect from a POSIX shell:

| Operator | Meaning |
|----------|---------|
| `cmd1 \| cmd2` | Pipe stdout of `cmd1` into stdin of `cmd2` |
| `cmd > file` / `>> file` | Redirect stdout (truncate / append) |
| `cmd < file` | Redirect stdin from a file |
| `cmd 2> file` / `2>> file` | Redirect stderr |
| `cmd 2>&1` | Merge stderr into stdout |
| `cmd1 ; cmd2` | Sequence (run both regardless of exit code) |
| `cmd1 && cmd2` | Run `cmd2` only if `cmd1` succeeded |
| `cmd1 \|\| cmd2` | Run `cmd2` only if `cmd1` failed |
| `*`, `?`, `**` | Glob expansion (recursive `**` supported) |
| `\` at end of line | Continue command on the next line (one history entry) |

```
eosh> ls *.py | grep test | wc -l
eosh> make 2>&1 | tee build.log
eosh> echo hello > out.txt && cat out.txt
```

Both registered Python commands and external programs work seamlessly inside pipelines.

### Pipeline Decorators

A **decorator** is a token of the form `@name [flags]` at the start of a line that wraps the rest of the line as a pipeline and modifies how it runs. The leading `@` keeps the syntax visually distinct so it never collides with a regular command name.

```
@watch ls                              # bare single-command body
@watch -n 1 {df -h | grep abc}         # braced body required when operators appear
@time {make && ./run-tests}
@retry -n 5 --delay 2 curl https://flaky.example.com/
@quiet pytest -q
```

**Scope rule.** If the wrapped pipeline contains `|`, `;`, `&&`, `||`, or a redirect, it must be enclosed in `{...}`. Single-command bodies don't need braces. This makes the decorator's scope visible at a glance and side-steps the `watch -n 5 ls | grep abc` ambiguity that POSIX `watch` is famous for.

**Built-in decorators:**

| Decorator | Description |
|-----------|-------------|
| `@watch [-n SEC] [--no-clear]` | Re-run a pipeline on a timer, showing its latest output (`q` quits) |
| `@time` | Print elapsed wall/user/sys time after the pipeline finishes |
| `@retry [-n N] [--delay SEC]` | Re-run the pipeline on non-zero exit, up to `N` attempts |
| `@quiet [--stderr]` | Discard stdout (and stderr with `--stderr`); still propagates the exit code |

See the [Custom Decorators](#custom-decorators) section below for authoring your own.

## Customization

Create `~/.eosh/config.py` to define custom commands and completers. This file is plain Python that imports from eosh. Use `reload` to apply changes without restarting, or `config edit` to open it in your editor and reload when you quit. `reload` starts from a clean slate: everything the previous run registered (commands, decorators, aliases, variables, the prompt, a custom notifier) is dropped before the config runs again.

A config can't replace a built-in (`cd`, `help`, `@watch`, the `notify` variable, …) by accident: registering one of those names prints a `config warning:` line and keeps the built-in. Pass `override=True` (`registry.command("cd", override=True)`, `var_registry.register(v, override=True)`) when you mean it; `help` lists your commands apart from the built-ins, and removing the override and running `reload` brings the built-in back.

```python
# ~/.eosh/config.py
from eosh.commands import registry, arg
from eosh.completion import Completer, Completion, ChoiceCompleter
from eosh.recipes import enable

# Enable TAB completion for system commands
enable("make", "git", "ssh")

class InstanceCompleter(Completer):
    def complete(self, ctx):
        account = ctx.args[0] if ctx.args else ctx.shell_context.get_var("ACCOUNT")
        # fetch instances for account...
        return [Completion(value="i-abc123", description="web-server-1")]

@registry.command(
    name="connect",
    help="SSH into an EC2 instance.",
    params=[
        arg("account", choices=["prod", "staging"]),
        arg("region",  choices=["us-east-1", "us-west-2"]),
        arg("instance_id", completer=InstanceCompleter()),
    ],
)
def connect(account, region, instance_id):
    import os
    os.system(f"ssh {instance_id}")
```

### Python-Backed Variables

Register a variable with `var_registry`, and the built-in `var` command, bare `NAME=VALUE` assignment and `$NAME` / `${NAME}` expansion all go through it — a Python-backed variable is read- and write-symmetric with `os.environ`. An `EnvVar` names one or more environment keys (the shell writes all of them, per context). For a value that should stay out of child processes, subclass `PyVar` (still per-context) or `GlobalVar` (one value for the whole shell).

```python
# ~/.eosh/config.py
from eosh import EnvVar, var_registry
from eosh.completion import ChoiceCompleter, CallbackCompleter

var_registry.register(EnvVar(
    "aws_region", keys=["AWS_REGION", "AWS_DEFAULT_REGION"],
    completer=ChoiceCompleter(["us-east-1", "us-west-2", "eu-west-1"]),
    description="AWS region — sets AWS_REGION + AWS_DEFAULT_REGION",
))
var_registry.register(EnvVar(
    "aws_profile", keys="AWS_PROFILE",
    completer=CallbackCompleter(lambda: ["default", "prod", "staging"]),
))
```

With the variables above:

```
eosh> var aws_region=us-west-2
eosh> echo $AWS_REGION
us-west-2
eosh> aws ec2 describe-instances --region $aws_region
```

### Custom Decorators

A decorator is a command named `@name`: register it with `registry.command("@name", ...)`, exactly as you would a command. The function receives the body as its first positional argument (a `Sequence`: the pipelines inside the braces, joined by `;` / `&&` / `||`) and the parsed flag namespace as kwargs; call `body.run()` to execute it and return the exit code.

```python
# ~/.eosh/config.py
import sys
import time
from eosh.commands import arg, registry
from eosh.pipeline import Sequence

@registry.command(
    "@repeat",
    help="Run the body N times, stopping early on the first failure.",
    params=[
        arg("-n", "--count", type=int, default=3, metavar="N",
            help="number of iterations (default 3)"),
        arg("--delay", type=float, default=0.0, metavar="SEC",
            help="seconds to sleep between iterations"),
    ],
)
def repeat(body: Sequence, *, count: int, delay: float) -> int:
    last = 0
    for i in range(1, count + 1):
        sys.stderr.write(f"@repeat: iteration {i}/{count}\n")
        last = body.run()
        if last != 0:
            return last
        if delay > 0 and i < count:
            time.sleep(delay)
    return last
```

Usage:

```
eosh> @repeat -n 5 --delay 1 ls
eosh> @repeat -n 3 {make && ./run-tests}
```

To share decorators across machines or teammates, put them in a module and import it from `config.py` — see [Your Own Recipes](#your-own-recipes).

### Talking to the User: `ctx`

A command that asks questions, offers a choice, or launches an interactive program declares `pass_context=True`. Its handler then receives a `CommandContext` as its first argument:

```python
@registry.command("deploy", params=[arg("env")], pass_context=True)
def deploy(ctx, env):
    target = ctx.choose(["web", "worker", "scheduler"], title="Deploy which target?")
    if target is None or not ctx.confirm(f"Deploy {target} to {env}?"):
        return 1
    return ctx.run_interactive(["make", "deploy", f"TARGET={target}"])
```

| Method | Does |
|---|---|
| `ctx.input(prompt)` / `ctx.input_block(prompt)` | read a line / a pasted block (up to a blank line) |
| `ctx.confirm(prompt, default=False)` | yes/no question; `[y/N]` is appended |
| `ctx.choose(items, title="")` | inline picker; returns the item, or `None` on Esc |
| `ctx.run_interactive(argv)` | run `ssh`, an MFA prompt, a TUI… on a terminal of its own |
| `ctx.get_var(name)` / `ctx.set_var(name, value)` / `ctx.unset_var(name)` | the context's variables, as `$name` / `var name=value` |
| `ctx.context_name`, `ctx.cwd` | where the command is running |

Use these instead of `input()` and `subprocess.run` for anything that reads the keyboard. A Python command runs on a background thread while the shell reads every key (that is what lets Ctrl+] switch away from it), so `input()` or an interactive `subprocess.run` would split keystrokes with the shell. Non-interactive subprocesses (`capture_output=True`, `pexpect.popen_spawn`, …) are fine as they are.

`ctx` belongs to the context the command **started in**. A command you send to the background with Ctrl+] still reads and sets that context's variables, so a script started in `prod` can't change `staging`.

### Desktop Notifications

When a command runs for at least 10 seconds, eosh posts an OS notification as it finishes — by then you've probably switched to a browser:

```
✓ eosh — 1m 23s          make -j8 release
✗ eosh — exit 2 (20.0s)  make
✓ eosh — 1h 05m          [bg-1] terraform apply
```

Commands running in a context you backgrounded with `Ctrl+]` notify too, tagged with the context name. Interactive programs — editors, pagers, `top`, `ssh`, `tmux`, sub-shells — are skipped, since sitting in them for an hour isn't work finishing.

Backends are whatever the platform already ships: `osascript` on macOS, `notify-send` on Linux, a PowerShell toast on Windows, and the terminal bell as a fallback. Nothing to install.

Control it at the prompt:

```
eosh> var notify=off              # disable for this session
eosh> var notify_threshold=30     # only notify for commands ≥ 30s
```

Or from your config:

```python
# ~/.eosh/config.py
from eosh import notify

notify.configure(threshold=30)
notify.SKIP_COMMANDS.add("psql")

# Or replace the backend entirely — Slack, ntfy.sh, tmux display-message, ...
notify.set_notifier(lambda title, message: post_to_slack(f"{title}\n{message}"))
```

See [doc/notifications.md](https://github.com/crftwr/eosh/blob/v0.3.0/doc/notifications.md) for the design and the known limits.

### Event Hooks

Run your own functions when something happens in the shell. Each event is a decorator from `eosh.hooks`:

```python
# ~/.eosh/config.py
import os
from eosh import hooks

@hooks.on_context_switched
def title(old, new):
    print(f"\x1b]2;[{new}] eosh\x07", end="", flush=True)   # terminal title

@hooks.on_directory_changed
def per_project(old, new):
    if os.path.exists(os.path.join(new, ".venv")):
        print("(this project has a .venv)")

@hooks.on_command_not_found
def auto_cd(argv):                 # type a directory name to cd into it
    if len(argv) == 1 and os.path.isdir(argv[0]):
        os.chdir(argv[0])
        return True                # claimed: no "command not found"
    return False
```

| Event | Called as |
|---|---|
| `on_startup` / `on_exit` | `f()` |
| `on_directory_changed` | `f(old, new)` — `cd`, a context switch, anything that moves the cwd |
| `on_context_switched` | `f(old, new)` — Ctrl+], `context switch` / `new` / `close` |
| `on_command_starting` | `f(line)` — just before a line runs (observe only) |
| `on_command_finished` | `f(line, status, elapsed)` |
| `on_command_not_found` | `f(argv) -> bool` — return `True` to claim the command |

Hooks run in registration order. A hook that raises is reported with its traceback, and the next one still runs. `reload` forgets them all before re-running the config. See [doc/hooks.md](https://github.com/crftwr/eosh/blob/v0.3.0/doc/hooks.md) for the details and [doc/limitations.md](https://github.com/crftwr/eosh/blob/v0.3.0/doc/limitations.md#event-hooks--what-they-dont-see) for the known gaps.

### Prompt Customization

The prompt is generated by a Python function you can override with `set_prompt()`. The default prompt shows the context name (if not `"default"`), current directory (up to 2 levels), a timestamp, and `[bg:N]` when N other contexts have running processes:

```
[prod] projects/eosh 14:32:07>
```

To customize, define a function that takes a `ContextManager` and returns a string:

```python
# ~/.eosh/config.py
import os
from datetime import datetime
from eosh import set_prompt

def my_prompt(context_manager):
    ctx = context_manager.current()
    prefix = f"({ctx.name}) " if ctx else ""
    cwd = os.path.basename(os.getcwd()) or "/"
    time = datetime.now().strftime("%H:%M")
    return f"{prefix}{cwd} [{time}]$ "

set_prompt(my_prompt)
```

The function is called each time the prompt is displayed, so it reflects dynamic state like the current directory, time, or context variables.

### Available Completers

| Completer | Description |
|-----------|-------------|
| `ChoiceCompleter(items)` | Complete from a static list |
| `CallbackCompleter(func)` | Complete from a function's return value |
| `FileCompleter()` | Complete filesystem paths (files and directories) |
| `DirCompleter()` | Complete directory paths only |
| `OptionsCompleter(options, args)` | Complete flags, one picker row each; `args` declares value-taking flags |

### Completion Recipes

Built-in recipes add TAB completion for common system commands. Enable them in `~/.eosh/config.py`:

```python
from eosh.recipes import enable
enable("git", "make", "ssh", "kill", "ls", "grep", "find", "du", "df", "tail", "aws")
```

Each recipe registers flag completion (via `OptionsCompleter`) and positional completions (subcommands, files, branches, etc.) for the named command.

Two protocols cover whole families of tools without a per-tool recipe:

- **Cobra-based tools** (`docker`, `kubectl`, `helm`, `gh`, `argocd`, …) — `CobraCompleter` drives their `__complete` subcommand, including live resource enumeration (running containers, k8s resources, GitHub issues, …). Opt-in by name: `enable("cobra")` covers the well-known tools, and `enable_cobra("mytool")` adds your own. See [doc/cobra.md](https://github.com/crftwr/eosh/blob/v0.3.0/doc/cobra.md).
- **argcomplete-based Python CLIs** (automatic) (`pipx`, `conda`, `pre-commit`, `tox`, `pdm`, `httpie`, …) — `ArgcompleteCompleter` detects the `# PYTHON_ARGCOMPLETE_OK` marker and drives the argcomplete protocol. See [doc/argcomplete-fallback.md](https://github.com/crftwr/eosh/blob/v0.3.0/doc/argcomplete-fallback.md).

#### Your Own Recipes

A recipe is plain Python: register a command with no handler, and its `params` drive completion for the external tool. Write it in `config.py`, or in a module next to it — `~/.eosh` is on `sys.path` while `config.py` runs, and `reload` re-runs the modules imported from there:

```python
# ~/.eosh/my_tools.py
from eosh.commands import arg, registry
from eosh.completion import CallbackCompleter

registry.command(
    "my-tool",
    help="my-tool — deploy/rollback/status helper",
    params=[
        arg("subcommand", choices=["deploy", "rollback", "status"]),
        arg("target", help="deploy target",
            completer=CallbackCompleter(lambda: ["web", "worker", "scheduler"])),
        arg("-v", "--verbose", action="store_true", help="verbose"),
        arg("--dry-run", action="store_true", help="don't apply changes"),
    ],
)
```

```python
# ~/.eosh/config.py
import sys
sys.path.append("/team/shared/eosh")   # optional: recipes shared by a team
from eosh.recipes import enable

enable("git")       # built-in
import my_tools     # your own, from ~/.eosh
```

A module you import may use third-party packages, but they must be installed
in eosh's own environment (see [Installation](#installation) for `uv tool` /
`pipx`).

`enable()` never stops your config: an unknown name, or a recipe whose
`register()` raises, is a `config warning:` line (the latter with its
traceback) and the remaining names still load. An error in a module of your
own — a typo in an import, a syntax error — is printed at startup with a
traceback through your files.

### Writing a Custom Completer

Subclass `Completer` and implement `complete()`. The `CompletionContext` gives you:

- `command` — the command being completed for
- `args` — previously completed arguments
- `arg_index` — which argument position is being completed
- `prefix` — partial text typed so far
- `shell_context` — the active context, read-only: `.get_var(name)`, `.context_name`, `.cwd`

To add completion to a system command without wrapping it, register a handler-less command — execution falls through to the real binary:

```python
from eosh.commands import arg, registry
from eosh.completion import FileCompleter

registry.command(
    "mytools",
    help="my custom tool",
    params=[
        arg("file", nargs="*", completer=FileCompleter()),
        arg("-v", "--verbose", action="store_true", help="verbose"),
        arg("--output", metavar="FILE", help="output file"),
    ],
)
```

### Key Bindings

| Key | Action |
|-----|--------|
| `Tab` | Open completion picker |
| `Ctrl+R` | Search history, starting from what's typed |
| `Ctrl+]` | Open context switcher |
| `↑` / `↓` or `Ctrl+P/N` | Navigate history |
| `Ctrl+A` / `Ctrl+E` | Move to start / end of line (`Ctrl+E` at the end accepts the suggestion) |
| `Ctrl+B` / `Ctrl+F`, `←` / `→` | Move one character left / right (at the end, accept the suggestion) |
| `Alt+B` / `Alt+F` | Move one word left / right (`Alt+F` at the end accepts one word of the suggestion) |
| `Ctrl+W` | Delete word before cursor |
| `Ctrl+K` | Delete to end of line |
| `Ctrl+U` | Delete to beginning of line |
| `Ctrl+L` | Clear screen |
| `Ctrl+D` | Exit (on empty line) |

## File Locations

| Path | Purpose |
|------|---------|
| `~/.eosh/config.py` | User configuration |
| `~/.eosh/history.db` | Command history, shared by every eosh process (SQLite) |
| `~/.eosh/*.py` | Your own modules (recipes, commands, decorators) — on `sys.path` while `config.py` runs |

## Platform Support

The interactive shell — line editing, completion, all TUI pickers, history, pipelines, redirects, built-ins, decorators, and `Ctrl+]` context switching at the prompt — runs natively on both POSIX and Windows. Path separators are normalized to `/` on every platform (Git-Bash style) so a path can never be mistaken for a `\`-line-continuation.

The one POSIX-only feature is **PTY-backed multiplexing of a live external process**: backgrounding a *running* native program with `Ctrl+]` and resuming it later. On Windows, external commands run on the real console with inherited stdio.

## License

MIT — see [LICENSE](https://github.com/crftwr/eosh/blob/v0.3.0/LICENSE).
