Metadata-Version: 2.4
Name: arbol-tree
Version: 0.1.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: 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.

Arbol has three input sources, all drawn by the same renderer:

1. A Python dictionary passed in directly
2. A JSON file read from disk — filesystem-shaped or not
3. A filesystem directory walked recursively

```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
│   ├── .hidden
│   │   ├── .DS_Store
│   │   ├── .swp
│   │   ├── build.log
│   │   ├── cache.tmp
│   │   ├── notes.txt.bak
│   │   ├── session.lock
│   │   └── Thumbs.db
│   ├── bundle.min.css
│   └── legacy.js
├── .env.example
├── config.yaml
└── README.md
```

Hidden entries show by default — `.hidden/` and `.env.example` are there
without asking. Directories sort before files, then case-insensitively.

## 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
```

It is not published yet, so for now run it from a path. From the project
directory:

```bash
uv run arbol .
```

With `uvx`, from anywhere:

```bash
uvx --from /path/to/arbol arbol .
```

Or build a wheel and run that:

```bash
uv build
```

```bash
uvx --from ./dist/arbol_tree-0.1.0-py3-none-any.whl 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 |
| --- | --- |
| `-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 |

Walking a directory includes **everything** by default — hidden entries and all
depths. That differs from `tree`, which hides dotfiles unless given `-a`. In a
repository, expect `.git` to show up.

### 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 |

The trailing slash is the useful one: it is the difference between hiding a
folder and hiding everything that shares its naming convention.

```bash
uv run arbol . -I '__*__/' -I '*.pyc'
```

That drops every `__pycache__` folder and every stray `.pyc`, while keeping
`__init__.py` — the slash confined the first pattern to directories, and the
second names files. Ignoring a directory ignores everything beneath it.

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

```bash
uv run arbol . -I '.*/|__*__/|node_modules'
```

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

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

### Drawing a JSON file

A JSON document needs no filesystem structure. Any nesting of objects, arrays
and strings draws:

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

```
Acme Corp
├── Engineering
│   ├── Backend
│   │   ├── api
│   │   └── workers
│   └── Frontend
│       ├── web
│       └── mobile
└── Operations
    └── Support
        └── tier-1
```

The `ROOT` key 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.

Saving the JSON from a directory walk and drawing it back round-trips:

```bash
uv run arbol . -o tree.json && uv run arbol tree.json
```

## 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.
- Nothing is hidden unless a pattern says so. There is no `-a`, because the
  default already shows every entry.

## 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).
