An interactive TUI for triaging files and folders out of an "inbox" directory into your filesystem - with pluggable scripts that suggest where things go.
TODO: replace with a real terminal screenshot of a decision screen (a few suggestions, the legend, and a preview).
- What it does
- Requirements
- Layout
- Tests
- Usage
- Configuration
- Action logs
- Writing a suggester script
- "Help me decide" (the decision tree)
For each entry directly under the root directory you get four choices:
- Accept a suggested location - generated by pluggable suggester scripts.
- Choose a folder manually - a built-in folder browser.
- Ignore - remembered across runs; never asked about again.
- Skip - this run only.
For a folder, you can also descend into it and organize its contents instead of moving the whole folder as one unit.
Each entry is shown with its full path, type, size, modification time, and a compact preview (a few lines for text, dimensions for images, item count and sample names for folders). The full path is printed so terminals that support it can offer their own preview/open popup.
- Python 3.11+ (uses
tomllib; developed against 3.14). - A terminal with
curses. On Linux/macOS this is part of the standard library; on Windows it isn't, so installing this project also pulls inwindows-cursesthere (and only there - it's declared with aplatform_system == 'Windows'marker inpyproject.toml, so Linux/macOS installs never see it). platformdirs- installed automatically as a regular dependency. It resolves this OS's default config location (see Configuration).- Optional:
Send2Trash- needed only for suggested deletions, which move items to the OS recycle bin. Without it, delete suggestions report that it's missing rather than deleting anything. Install it with thetrashextra (see below).
pipx install -e ".[trash]" # drop ".[trash]" to skip the recycle-bin extrapipx is the standard way to install a Python CLI
tool: it puts org-assistant on your PATH while keeping its dependencies in
their own isolated environment, with no venv to remember to activate. Install
pipx itself once with pip install --user pipx && pipx ensurepath (or your OS
package manager) if you don't have it yet.
Prefer a plain venv instead?
python -m venv .venv
source .venv/bin/activate
pip install -e ".[trash]"Prefer not to install anything? Run it straight from a checkout instead:
python -m org_assistant [ROOT] [-c FOLDER] [-ls] [-s] [--ignore-certain]org_assistant/
cli.py CLI entry point (argument parsing); installed as the `org-assistant` command
__main__.py lets you run it uninstalled: python -m org_assistant
config.py load configuration
suggesters.py load suggester scripts and gather suggestions
decision.py the "Help me decide" tree
fileinfo.py per-entry details + preview
actions.py ignore list, moving entries, recycle-bin deletion
ui.py the curses interface
cfgs/
example/ tracked reference setup - copy it wherever your own setup should
live (see Configuration for where that is)
config.toml settings (base_dir + colours)
decision_tree.toml the "Help me decide" tree
suggesters/ suggester scripts
ignored.json ignore list (created at runtime)
logs/ action logs (created at runtime)
tests/ unittest suite (see Tests)
The suite uses the standard-library unittest - no dependencies. From the repo
root:
python -m unittest discover -s tests -t .The UI-helper tests need curses and are skipped where it isn't available (e.g.
Windows without windows-curses); everything else runs anywhere. CI
(.github/workflows/tests.yml) runs this on Python 3.11–3.13 on every push and
pull request.
org-assistant [ROOT] [-c FOLDER] [-ls] [-s] [--ignore-certain]ROOT- the inbox directory. Only entries directly under it are processed (no recursion into subfolders). Required to organize; omit it for-ls/-s.--config-folder,-c FOLDER- the config folder to use (see Configuration for how it's resolved when omitted).--list-suggesters,-ls- print the loaded suggester scripts and exit.--summary,-s- print a summary of your manual actions (from the logs) and exit (see Action logs).--ignore-certain- ignore theCERTAINshort-circuit and run every suggester even after one is certain (by default a certain suggestion stops the rest - see below).
Per-entry screen. The suggestions plus manual, help, and skip form one list you move through with the arrow keys; each also has a quick-pick key (highlighted inside its label).
| Key | Action |
|---|---|
↑/↓ or k/j |
move the highlight through the whole option list |
Enter |
activate the highlighted option |
1–9 |
quick-pick a suggestion by number |
m |
choose a target folder manually |
d |
descend into the folder and organize its contents (directories only) |
h |
"Help me decide" - walk a decision tree (see below) |
s |
skip (this run only) |
Del |
delete - move the file/folder to the OS recycle bin (confirmed) |
Backspace |
ascend - stop and go back up (only while inside a descended folder) |
i |
ignore (remember forever) |
q |
quit |
Descending processes each item inside the folder with the same screen (nesting as
deep as you go). Press Backspace to ascend back up without stepping through the
rest of the items; when a folder's listing ends you return to that folder's own
screen - now showing its updated contents - so you can move, skip, or ignore it.
Manual folder browser:
| Key | Action |
|---|---|
↑/↓ |
navigate |
Enter/→ |
open folder |
Backspace/← |
go up |
d |
drop the entry into the current folder |
n |
create a new subfolder |
Esc |
cancel |
If the destination already exists you're asked whether to keep both (the moved
item gets a (n) suffix).
Everything for one setup lives in a config folder. A folder contains:
config.toml settings (base_dir + [colors])
decision_tree.toml the "Help me decide" tree (optional; omit to disable)
suggesters/ the suggester scripts for this setup
ignored.json the ignore list (created at runtime)
logs/ action logs (created at runtime)
cfgs/example/ in this repo is the tracked reference - copy it to wherever you
want a setup to live. Which folder gets used is resolved in this order:
--config-folder/-c FOLDER, if given../.org-assistant, if it exists in the current directory - a project directory can carry its own setup that's picked up automatically from inside it.- Otherwise, this OS's default config location (via
platformdirs-~/.config/org-assistanton Linux,~/Library/Application Support/org-assistanton macOS,%APPDATA%\org-assistanton Windows). Since this one isn't visible from wherever you happen to be standing, the CLI prints the path whenever it's the one used.
# One inbox you always run from the same project directory:
cp -r cfgs/example ./.org-assistant
org-assistant ~/inbox # picked up automatically
# One inbox, used from anywhere - the OS default:
cp -r cfgs/example ~/.config/org-assistant # path shown above is Linux; adjust per OS
org-assistant ~/inbox # falls back to it automatically
# Any number of additional setups, selected explicitly:
cp -r cfgs/example ~/configs/work
org-assistant ~/work-inbox -c ~/configs/workOnly cfgs/example is tracked in git - every setup you make from it (wherever
it lives) is yours, so your suggesters and ignore lists never end up in the
repo.
config.toml holds just two things (see cfgs/example/config.toml):
| Key | Meaning | Default |
|---|---|---|
base_dir |
root of your organized files: where [m] browsing starts, and offered to suggesters |
~/ |
[colors] |
optional per-role colour overrides (fg or fg on bg) |
the built-in palette |
Every move and deletion is logged (as JSON Lines) under
the active config folder's logs/, split by who decided:
logs/manual.jsonl- what you sorted by hand: "choose a folder manually" and "Help me decide" moves.logs/suggester.jsonl- moves and deletions accepted from suggester scripts.
Each line records the time, action (move/delete), via
(manual / help / suggester:<name>), the source path and name, and (for moves)
the destination. Logs are per-config runtime state and are git-ignored.
Reviewing the manual log is the point: it shows what you keep sorting by hand, so recurring patterns can become new suggester scripts. Get a grouped overview with:
org-assistant -s # or: -s -c /path/to/another/setupwhich prints your manual moves grouped by destination folder (with example filenames), most frequent first - e.g. "12 → ~/Docs/Invoices e.g. inv_jan.pdf, …" tells you a "invoices go here" suggester is worth writing.
Suggesters live in the config folder's suggesters/ directory. Each is a .py
file (names starting with _ are skipped) that defines a suggest(path) function
and, optionally, a HANDLES attribute. Every entry is passed to every applicable
suggester.
from pathlib import Path
HANDLES = "file" # "file", "dir", or "both" (default: "both")
def suggest(path: Path):
# `path` is a pathlib.Path to the inbox entry.
...To read configuration, declare a second parameter - it receives the Config
object, so you can build destinations relative to base_dir:
def suggest(path, config):
return {"dir": config.base_dir / "Invoices", "confidence": "high"}One-argument suggesters keep working unchanged; the config is passed only when the function accepts it.
Return one of the following, or a list of them for multiple suggestions:
| Return | Meaning |
|---|---|
False / None |
no suggestion from this script |
"~/Docs" (str) |
target directory; original filename kept |
(dir, name) (tuple) |
target directory and new filename |
{"dir": ..., "name": ..., "label": ...} |
a move; name and label optional |
{"delete": True, "label": ...} |
a deletion (moves to the OS recycle bin); label optional |
Any dict may also carry "confidence" (see below).
Notes:
- A
listmeans "several suggestions"; atuplemeans one(dir, name). labelis what the user sees in the list; if omitted it's derived from the path.- Paths may use
~and are expanded. Relative paths are allowed but absolute is clearer. - Duplicate suggestions (same action/target/name) across scripts are de-duplicated.
- Exceptions in a suggester are caught and shown as an error - one bad script won't crash the app.
A dict suggestion may include "confidence" - a level name ("low", "medium",
"high", "certain"), the matching integer 1–4, or the Confidence enum
from org_assistant.suggesters. It defaults to "medium".
- Suggestions are presented sorted by confidence, highest first.
"certain"is special: it means the suggester is sure, so the remaining suggesters are not run. Pass--ignore-certainto run them anyway.
The confidence is shown next to each suggestion, e.g. [by_extension · high].
Return {"delete": True} (optionally with a "label" and "confidence") to
suggest sending the entry to the OS recycle bin. Accepting it asks for
confirmation, then uses Send2Trash (see Requirements). Nothing is ever
permanently deleted by org-assistant.
Where suggesters guess automatically, the h option walks you through questions
you define, then hands off to the folder browser. It's driven by the
decision_tree.toml in your config folder (see cfgs/example/decision_tree.toml);
delete that file to disable the feature for a setup.
A tree is a set of named nodes. Each node has a question and a list of
choices; every choice has a label and exactly one of:
| Field | Meaning |
|---|---|
goto = "<node id>" |
jump to another question |
target = "<folder>" |
a leaf - opens the folder browser rooted at <folder> so you pick the exact subfolder, then the file is moved there |
root names the starting node. Paths may use ~.
root = "start"
[nodes.start]
question = "What kind of item is this?"
choices = [
{ label = "Work document", goto = "work" },
{ label = "Personal photo", target = "~/Media/Photos" },
]
[nodes.work]
question = "Which project?"
choices = [
{ label = "Project Apollo", target = "~/Work/Apollo" },
]Navigate with numbers/arrows, Enter to choose, Backspace to go up a question,
Esc to cancel. If the file is missing or invalid, the option reports it instead
of running; validation errors are shown on the startup screen.
See cfgs/example/suggesters/ for working examples - 10_by_extension.py
(config-relative destinations + confidence), 20_dated_media.py (files+dirs,
multiple suggestions, rename), and 30_junk.py (a deletion + CERTAIN confidence).
They're active in any setup copied from cfgs/example.
