Metadata-Version: 2.4
Name: CTkKanBan
Version: 2.0.1
Summary: A small, predictable Kanban board for CustomTkinter
Author-email: Harry Gomm <Harry-g25@users.noreply.github.com>
License-Expression: MIT
Project-URL: Source, https://github.com/Harry-g25/CTkKanBan
Project-URL: Issues, https://github.com/Harry-g25/CTkKanBan/issues
Keywords: customtkinter,kanban,desktop,gui
Classifier: Development Status :: 4 - Beta
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
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: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: customtkinter<7,>=5.2.2
Provides-Extra: test
Requires-Dist: pytest>=8.3; extra == "test"
Provides-Extra: dev
Requires-Dist: build>=1.2; extra == "dev"
Requires-Dist: mypy>=1.14; extra == "dev"
Requires-Dist: pytest>=8.3; extra == "dev"
Requires-Dist: ruff>=0.9; extra == "dev"
Requires-Dist: tox>=4.23; extra == "dev"
Requires-Dist: twine>=6; extra == "dev"
Dynamic: license-file

# CTkKanban

CTkKanban is a small Kanban widget for CustomTkinter. It focuses on predictable
card editing and movement instead of trying to be a database framework or a
complete project-management application.

## Install

```bash
python -m pip install CTkKanBan
```

Import the package with its canonical lowercase name:

```python
import customtkinter as ctk

from ctk_kanban import CTkKanbanBoard

app = ctk.CTk()
app.geometry("1000x650")

board = CTkKanbanBoard(
    app,
    columns=[
        {"id": "todo", "title": "To do"},
        {"id": "doing", "title": "Doing"},
        {"id": "done", "title": "Done"},
    ],
    cards=[
        {
            "id": 1,
            "column": "todo",
            "title": "Try the simplified board",
            "description": "Click for details and use the handle to drag.",
            "priority": "High",
            "tags": ["demo"],
        }
    ],
    on_change=lambda event: print(event["type"], event["data"]),
)
board.pack(fill="both", expand=True)
app.mainloop()
```

## Interaction model

- Click a card to select it and open the editor drawer.
- Save explicitly with **Save changes** or Enter; Escape cancels.
- Drag cards only from their upper-right drag handle.
- Use the visible menu for move and delete actions.
- Columns use menu actions for left/right movement instead of column dragging.

There is no inline editing, click-away autosave, whole-card dragging, floating
drag preview, or window-wide drag binding. A local Tk grab makes sure a handle
drag always receives its release event.

## Styling

Board surfaces, controls, text, hover states, and scrollbars follow the active
CustomTkinter color theme. Call `ctk.set_default_color_theme(...)` before
creating the board to use another built-in or custom theme. Priority and tag
metadata remain visible as compact colored pills. The optional `theme` mapping
can override individual board tokens when needed.

## Data

Columns contain `id` and `title`. Cards contain `id`, `column`, `title`, and the
optional `description`, `priority`, and `tags` fields. IDs must be unique and
must be nonblank strings or integers. Priorities are empty, `Low`, `Medium`,
`High`, or `Critical`. Tags are trimmed, nonblank strings without commas.

`get_data()` returns a detached snapshot for application-owned storage. Use
string or integer IDs when the snapshot will be encoded as JSON.
`set_data(snapshot)` replaces the displayed board without emitting an event.
The optional `on_change` callback receives one event after each successful
add, edit, move, or delete that changes board data; `event["data"]` contains
the latest complete snapshot. Search also changes only the view and does not
emit an event.

Persistence, retries, paging, polling, and conflict handling intentionally live
in the host application rather than the widget.

### Database rows

Database results can be converted without adding a database-driver dependency
to CTkKanban. Mapping rows from psycopg `dict_row`, `sqlite3.Row`, and
SQLAlchemy are accepted by `snapshot_from_rows()`:

```python
from ctk_kanban import snapshot_from_rows

snapshot = snapshot_from_rows(column_rows, card_rows)
board.set_data(snapshot)
```

Plain DB-API tuple results can be converted using cursor metadata. Fetch each
result before reusing its cursor:

```python
from ctk_kanban import rows_from_cursor, snapshot_from_rows

cursor.execute("SELECT id, title FROM kanban_columns ORDER BY position")
columns = rows_from_cursor(cursor)

cursor.execute(
    """
    SELECT id, column_id AS column, title, description, priority, tags
    FROM kanban_cards
    ORDER BY column_id, position
    """
)
cards = rows_from_cursor(cursor)

board.set_data(snapshot_from_rows(columns, cards))
```

Use SQL aliases such as `column_id AS column` to produce CTkKanban's exact
record keys. Result column names must be unique. `rows_from_cursor()` consumes
all remaining rows returned by the cursor.

`snapshot_from_cursors(columns_cursor, cards_cursor)` is a shorter equivalent
when two separately executed cursors are available. Every snapshot helper
normalizes and validates the complete result before returning it.

### Asynchronous loading

`load_async()` performs fetching and validation on a daemon worker, then calls
`set_data()` and user callbacks safely on Tk's thread:

```python
import psycopg
from psycopg.rows import dict_row

from ctk_kanban import snapshot_from_rows


def fetch_board():
    with psycopg.connect(DATABASE_URL, row_factory=dict_row) as connection:
        columns = connection.execute(COLUMN_QUERY).fetchall()
        cards = connection.execute(CARD_QUERY).fetchall()
        return snapshot_from_rows(columns, cards)


board.load_async(
    fetch_board,
    on_success=lambda snapshot: print("Loaded", len(snapshot["cards"]), "cards"),
    on_error=lambda error: print("Load failed:", error),
)
```

`board.is_loading` reports pending work and `board.load_error` retains the most
recent asynchronous error. Existing data is preserved on failure unless
`clear_on_error=True` is requested. Starting a newer load makes an older result
stale, so it cannot overwrite newer data.

Pass `on_card_open` when the host application owns card editing. Its callback
receives the card snapshot and replaces the built-in drawer when a card opens.

## Main API

```text
get_data() / set_data(data)
get_card(id) / get_cards(column_id=None) / get_columns()
add_card() / update_card() / move_card() / delete_card()
add_column() / update_column() / move_column() / delete_column()
open_add_card_editor() / open_edit_card_editor(id)
search(query)
set_loading(bool) / load_async(fetch_snapshot, ...)
rows_from_cursor(cursor)
snapshot_from_rows(columns, cards) / snapshot_from_cursors(columns_cursor, cards_cursor)
```

The Tk-free `BoardModel` is also public for applications that want to validate
or manipulate board data without creating a window. `Column.from_definition()`
and `Card.from_definition()` expose the same normalization for typed application
code, while `BoardSnapshot`, `ColumnRecord`, and `CardRecord` provide public
typing shapes.

## Migrating from 1.x

Version 2 is intentionally breaking. Remove dynamic field definitions, inline
editing options, persistence adapters, advanced filter/sort options, and the
large set of `enable_*`/`show_*` constructor flags. Replace mutation-specific
callbacks with `on_change`, and import from `ctk_kanban` rather than
`CTkKanBan`. Remove custom record keys before loading data; v2 rejects fields
outside its small schema instead of silently discarding them.

## Development

```bash
python -m pip install -e ".[dev]"
python -m pytest -q
python -m ruff check .
python -m mypy ctk_kanban
```
