Metadata-Version: 2.4
Name: arbol-tree
Version: 0.2.0
Summary: Draw tree structures in the terminal, from a directory, a JSON file, or a Python dict.
Keywords: tree,terminal,cli,directory,json,visualization
Author: flapjackstan
License-Expression: MIT
License-File: LICENSE
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Utilities
Classifier: Typing :: Typed
Requires-Dist: rich>=15.0.0
Requires-Dist: typer>=0.27.0
Requires-Python: >=3.11
Project-URL: Homepage, https://github.com/flapjackstan/arbol
Project-URL: Documentation, https://flapjackstan.github.io/arbol/
Project-URL: Repository, https://github.com/flapjackstan/arbol
Project-URL: Issues, https://github.com/flapjackstan/arbol/issues
Description-Content-Type: text/markdown

# Arbol

Draw tree structures in the terminal, in the style of the Linux `tree` command
— on Windows, macOS and Linux, with the same commands everywhere.

```console
$ arbol tests/data/sample_tree -I 'datasets|assets|suite|scripts' -L 3
tests/data/sample_tree
├── docs
│   ├── images
│   │   ├── diagram.svg
│   │   └── logo.png
│   ├── api.md
│   ├── changelog.md
│   └── guide.md
├── src
│   ├── core
│   │   ├── utils
│   │   ├── engine.py
│   │   └── parser.py
│   ├── plugins
│   │   ├── export.py
│   │   └── importer.py
│   └── __init__.py
├── vendor
│   ├── bundle.min.css
│   └── legacy.js
├── config.yaml
└── README.md
```

Hidden entries are skipped by default — no `.hidden/`, no `.env.example`, no
`__pycache__/`. Pass `-a` to see them. Directories sort before files, then
case-insensitively.

## What it's for

Four uses, one renderer behind all of them.

### 1. `tree`, the same on every platform

`tree` is a Linux tool. Getting it on Windows means Chocolatey, WSL, or Git
Bash, and the flags differ once you do. Arbol is a Python package, so it
installs the same way everywhere and takes the same commands everywhere:

```bash
arbol .
```

```bash
arbol . -a -L 2
```

Output matches GNU `tree` — with `-a`, byte-identical to `tree -a --dirsfirst`
apart from the summary footer. Flag names follow `tree`'s, so `-L`, `-I` and
`-a` mean what you already expect.

### 2. Save the structure, edit it, draw it again

`-o` writes the walk to JSON. Edit that file however you like, then point arbol
back at it:

```bash
arbol . -o tree.json
```

```bash
arbol tree.json
```

Useful for proposing a layout before building it — add the directories you
*plan* to add, delete what you plan to remove, and render the result. The
edited file goes through exactly the same renderer as a real walk, so what you
see is what a finished tree would look like.

### 3. Draw a tree you wrote yourself

The JSON needs no filesystem behind it. Any nesting of objects, arrays and
strings draws, so you can describe an org, a schema, a taxonomy — anything
tree-shaped:

```json
{
  "ROOT": "Acme Corp",
  "Engineering": {"Backend": ["api", "workers"], "Frontend": ["web", "mobile"]},
  "Operations": {"Support": "tier-1"}
}
```

```console
$ arbol acme.json
Acme Corp
├── Engineering
│   ├── Backend
│   │   ├── api
│   │   └── workers
│   └── Frontend
│       ├── web
│       └── mobile
└── Operations
    └── Support
        └── tier-1
```

### 4. Draw a Python dict directly

No file, no CLI — import it and pass the dict:

```python
import arbol

arbol.print_tree(
    {
        "ROOT": "Acme Corp",
        "Engineering": {"Backend": ["api", "workers"], "Frontend": ["web", "mobile"]},
        "Operations": {"Support": "tier-1"},
    }
)
```

Same output as above. Use `arbol.render(...)` instead to get it back as a
string — useful for logs, tests, or writing into a report.

## Installing

The distribution is named **`arbol-tree`**, since `arbol` on PyPI is an
unrelated package. The import name and the command are both `arbol`.

```bash
pip install arbol-tree
```

### As a uv tool

To get an `arbol` command on your PATH without putting it in any project's
environment:

```bash
uv tool install arbol-tree
```

```bash
arbol .
```

Upgrade or remove it later with `uv tool upgrade arbol-tree` and
`uv tool uninstall arbol-tree`.

### Without installing anything

`uvx` fetches, runs, and discards in one step — handy for a one-off, or for
trying it before committing to an install:

```bash
uvx --from arbol-tree arbol .
```

> **The `--from` is required**, and it is not optional boilerplate. A bare
> `uvx arbol` resolves the **unrelated** `arbol` package on PyPI and fails with
> `Package 'arbol' does not provide any executables`. `uvx` assumes the package
> name matches the command name; here it does not.

Pin a version the same way:

```bash
uvx --from arbol-tree==0.1.0 arbol .
```

### From a checkout

```bash
uv run arbol .
```

## Command line

```
arbol [OPTIONS] [PATH]
```

`PATH` is a directory to walk or a JSON file to read, and defaults to the
current directory. Arbol works out which it is; there is no mode flag.

| Option | Description |
| --- | --- |
| `-a`, `--all` | Show hidden entries: dot names, and dunder folders |
| `-L`, `--level` | Maximum display depth, counting levels below the root |
| `-I`, `--ignore` | Wildcard pattern to skip, at any depth. Repeatable |
| `-o`, `--output` | Save the intermediate JSON here instead of a temporary file |
| `-V`, `--version` | Show the version and exit |

### What is hidden by default

Two rules, so the common case needs no flags at all:

- **anything starting with a dot** — `.git/`, `.venv/`, `.gitignore`,
  `.env` — files and folders alike, exactly as `tree` behaves without `-a`
- **dunder folders** — `__pycache__/`, `__snapshots__/` — build residue rather
  than content. Dunder *files* like `__init__.py` are source, and stay

`tree` does not have that second rule; it is the one place arbol's default
deliberately goes further. `-a` turns both off:

```bash
arbol . -a
```

With `-a`, arbol's output is byte-identical to `tree -a --dirsfirst` apart from
the summary footer.

### Ignore patterns

`-I` takes the same wildcard patterns as `tree -I`, matched against each
entry's **name** — never its path, so `-I 'src/*.pyc'` matches nothing, exactly
as in `tree`.

| Operator | Matches |
| --- | --- |
| `*` | Zero or more characters |
| `?` | Any single character |
| `[abc]`, `[a-z]` | One character from the set |
| `[^abc]` | One character not in the set |
| <code>&#124;</code> | Either alternate |
| trailing `/` | Restricts the pattern to directories |

`-I` is for what the default does not already cover — build output, vendored
trees, anything noisy that is not hidden:

```bash
arbol . -I '*.pyc' -I node_modules
```

The trailing slash is the subtle one: it is the difference between hiding a
folder and hiding everything that shares its naming convention. Ignoring a
directory ignores everything beneath it.

`-I` is repeatable, and `|` does the same job inside a single pattern:

```bash
arbol . -I '*.pyc|node_modules|dist'
```

Matching is case-sensitive, as in `tree` without `--ignore-case`.

> **On Windows, use `python -m arbol` when passing a wildcard.** The installed
> `arbol.exe` expands wildcards before Python runs, so `-I '*.pyc'` arrives as
> the list of files it matched and the command fails with
> `Got unexpected extra argument(s)`. Quoting does not help; the expansion
> happens below the shell. Plain `arbol .` is unaffected — the default rule
> needs no pattern — and so are patterns without wildcards, like
> `-I node_modules`.

The walk options describe a walk, so passing them alongside a JSON file is an
error rather than a silent no-op.

### The JSON format

Both the saved-and-edited case and the hand-written case use one format —
objects, arrays and strings, nothing else. `ROOT` names the root node and is
not drawn as a branch. Everything else follows three rules: an object
contributes one branch per key, an array contributes one child per item, and
anything else becomes a leaf.

For a walked directory that means:

- a **directory** is a list of entries
- a **file** is a string in that list
- a **subdirectory** is a single-key dict, `{"name": [...]}`

One quirk when hand-editing: **top-level entries are keys, deeper ones are
not.** A top-level file is written `"README.md": []` — an empty list renders as
a leaf — while a nested file is a plain string inside its parent's list. Files
have two representations depending on depth.

Two things to know about saving:

- the file holds **what was walked**, so hidden entries are absent unless you
  save with `-a`
- `ROOT` records the real directory name, not the path you typed —
  `arbol . -o tree.json` displays `.` but writes `"ROOT": "myproject"`

## Python API

Two objects, kept deliberately apart. `JsonBuilder` decides *what* is in the
tree; `ArbolTerminalView` draws exactly what it is given and filters nothing.

```python
import arbol

builder = arbol.JsonBuilder(ignore_patterns=["__*__/", "*.pyc"], level=2)
json_path = builder.write_directory("some/dir")

view = arbol.ArbolTerminalView()
print(view.render(arbol.JsonBuilder.load(json_path)))
```

Module-level shortcuts cover the simple cases:

```python
arbol.render({"ROOT": "r", "a": ["x", "y"]})
arbol.print_tree(arbol.load_json("tree.json"))
arbol.write_directory_json("some/dir", level=2)
```

By default `write_directory` puts the JSON in a temporary folder and leaves it
there for you to clean up. Pass `output_path` to save it somewhere real.

## Behavior worth knowing

- Symlinked directories are listed but never followed, so a symlink loop cannot
  blow up the output. `tree` behaves the same without `-l`.
- A directory that cannot be read is drawn as empty rather than aborting the
  walk.
- Entries sort directories first, then files, case-insensitively — the same
  order as `tree --dirsfirst`.
- `ignore_patterns` matches names at any depth, never paths, and applies to
  files and directories alike unless a trailing `/` narrows it.
- Dot names and dunder folders are hidden unless `-a` is given. `-I` patterns
  apply on top, and still apply with `-a`.

## Development

```bash
uv sync
```

```bash
uv run pytest
```

```bash
uv run ruff check .
```

```bash
uv run ruff format .
```

Releasing to PyPI, including the TestPyPI rehearsal, is written up in
[RELEASING.md](RELEASING.md).

## License

MIT. See [LICENSE](LICENSE).
