Skip to content

Latest commit

 

History

14 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

org-assistant

python 3.11+ dependencies optional tests

An interactive TUI for triaging files and folders out of an "inbox" directory into your filesystem - with pluggable scripts that suggest where things go.

org-assistant deciding where an inbox file goes

TODO: replace with a real terminal screenshot of a decision screen (a few suggestions, the legend, and a preview).


For each entry directly under the root directory you get four choices:

  1. Accept a suggested location - generated by pluggable suggester scripts.
  2. Choose a folder manually - a built-in folder browser.
  3. Ignore - remembered across runs; never asked about again.
  4. 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.

Requirements

  • 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 in windows-curses there (and only there - it's declared with a platform_system == 'Windows' marker in pyproject.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 the trash extra (see below).

Install

pipx install -e ".[trash]"      # drop ".[trash]" to skip the recycle-bin extra

pipx 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]

Layout

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)

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.

Usage

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 the CERTAIN short-circuit and run every suggester even after one is certain (by default a certain suggestion stops the rest - see below).

Keys

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

Configuration

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:

  1. --config-folder / -c FOLDER, if given.
  2. ./.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.
  3. Otherwise, this OS's default config location (via platformdirs - ~/.config/org-assistant on Linux, ~/Library/Application Support/org-assistant on macOS, %APPDATA%\org-assistant on 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/work

Only 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

Action logs

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/setup

which 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.

Writing a suggester script

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 value

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 list means "several suggestions"; a tuple means one (dir, name).
  • label is 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.

Confidence levels

A dict suggestion may include "confidence" - a level name ("low", "medium", "high", "certain"), the matching integer 14, 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-certain to run them anyway.

The confidence is shown next to each suggestion, e.g. [by_extension · high].

Deletions

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.

"Help me decide" (the decision tree)

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.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages